Pull to refresh
0
Adapty
Сервис для аналитики и роста мобильных подписок

iOS in-app purchases, часть 6: как реализовать скидки introductory offer, promotional offer, offer code

Reading time12 min
Views4.6K

Привет! Я Андрей, тимлид мобильной разработки в Adapty, я делаю SDK под iOS

В нашей серии статей мы с командой рассказываем о внедрении покупок на iOS, и это шестая статья из серии. Познакомиться с остальными можно по ссылкам:

  1. iOS in-app purchases, часть 1: Конфигурация и добавление в проект.

  2. iOS in-app purchases, часть 2: Инициализация и обработка покупок.

  3. iOS in-app purchases, часть 3: Серверная верификация покупок.

  4. iOS in-app purchases, часть 4: Тестирование покупок.

  5. iOS in-app purchases, часть 5: Обработка ошибок.

  6. iOS in-app purchases, часть 6: Скидки для iOS in-apps purchases. — Вы тут.

Скидки помогают приложениям привлечь новых подписчиков и удержать тех, кто потерял интерес к продукту. В Apple довольно большой инструментарий для их реализации, и сегодня я расскажу, какие возможности можно использовать, чтобы предоставить пользователям скидку: introductory offers, promo offers, offer codes, и проч.

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

Что такое скидочное предложение (offer)?

Offer (в статье иногда буду называть его cкидочным предложением) — это снижение цены на подписки для пользователей. Есть несколько вариантов:

  • стартовое предложение — introductory offer;

  • промо предложение — promo offer — скидка уже подписанному пользователю, например, при переходе на более дорогую подписку

  • возможность активировать промокод — offer code, который гарантирует определенную фиксированную цену при покупке.

Варианты оплаты: free trial, pay as you go, pay upfront

Каждое скидочное предложение имеет один из доступных способов оплаты.

Free trial — бесплатное использование

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

Pay as you go — оплата по мере использования

Подписчик платит сниженную цену каждый период подписки на протяжении какого-то времени. По истечении пробного периода списание происходит по обычной цене. Пример: три месяца со скидкой в 50%.

Pay up front — оплата наперёд

Пользователь единовременно оплачивает подписку на какой-то период. После окончания этого периода списание происходит по обычной цене. Пример: оплата сразу за год вперед со скидкой в 70%.

Стартовое предложение — introductory offer

Как работает introductory offer

Стартовое предложение позволяет пользователю ознакомиться с вашим приложением в течение какого-то фиксированного периода бесплатно или со скидкой, как упоминали выше: free trial, pay as you go, pay upfront.

Каждая подписка может иметь только одно стартовое предложение, которое в целом нельзя регулировать — Apple сам определяет, может ли пользователь применить эту скидку или нет. Чтобы на это влиять, есть более сложные сценарии, например, создать набор продуктов со скидкой и без и показывать разные продукты пользователю в зависимости от каких-то внутренних признаков, но это уже тема для другой статьи.

Реализация introductory offer

Для использования таких скидок нужно сначала настроить их в личном кабинете. Далее адаптировать интерфейс для отображения подобных скидок:

Шаг 1. Используя SKProductsRequest, нужно получить список продуктов из магазина:

class PaywallViewController: UIViewController {
    let productIds: Set<String> = ["<product_id>", "<product_id>"]
    var productsRequest: SKProductsRequest?
 
    override func viewDidLoad() {
        super.viewDidLoad()
        productsRequest = SKProductsRequest(productIdentifiers: productIds)
        productsRequest?.delegate = self
        productsRequest?.start()
    }
}
 
extension PaywallViewController: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        // your products are available here
        // let’s take a very first one as an example
        formatIntroductoryOfferMessage(for: response.products.first!)
    }    
 
    func request(_ request: SKRequest, didFailWithError error: Error) {
        // can’t fetch products
    }
}

Шаг 2. Далее найти нужный продукт, внутри которого будет лежать поле introductoryPrice, которое содержит всю информацию о вводном предложении, и сформировать цену скидки, ее длительность и проч. После отобразить это на экране оплаты:

Формирование локализованных заголовков для цены и длительности скидки
extension ViewController {
    func formatIntroductoryOfferMessage(for product: SKProduct) {
        let introductoryPrice = product.introductoryPrice
        let locale = product.priceLocale
        
        let localizedPrice = introductoryPrice?.price.localizedPrice(for: locale)
        self.discountLabel.text = localizedPrice // ex.: $10 or $9.99
        
        let localizedSubscriptionPeriod = introductoryPrice?.subscriptionPeriod.localizedPeriod(for: locale)
        discountDurationLabel.text = localizedSubscriptionPeriod // ex.: 3 weeks
    }
}
 
extension NSDecimalNumber {
    func localizedPrice(for locale: Locale) -> String? {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = locale
        formatter.minimumFractionDigits = 0
        formatter.maximumFractionDigits = 2
        return formatter.string(from: self)
    }
}
 
extension Locale {
    func localizedComponents(day: Int? = nil, weekOfMonth: Int? = nil, month: Int? = nil, year: Int? = nil) -> String? {
        // format and return localized period, like "3 weeks", "1 day", "2 years", etc
    }
}
 
@available(iOS 11.2, macOS 10.13.2, *)
extension SKProductSubscriptionPeriod {
    func localizedPeriod(for locale: Locale) -> String? {
        switch unit {
        case .day:
           // apple treats 1 week as 7 days so far
            if numberOfUnits == 7 { return locale.localizedComponents(weekOfMonth: 1) }
            return locale.localizedComponents(day: numberOfUnits)
        case .week:
            return locale.localizedComponents(weekOfMonth: numberOfUnits)
        case .month:
            return locale.localizedComponents(month: numberOfUnits)
        case .year:
            return locale.localizedComponents(year: numberOfUnits)
        @unknown default:
            return nil
        }
    }
}

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

Проверка доступности introductory offer

Чтобы проверить доступность вводного предложения для пользователя, нужно изучать его рецепт с покупками. Для этого необходимо провалидировать его на стороне сервера, а далее смотреть на признаки is_trial_period и is_in_intro_offer_period для всех покупок. Если хотя бы где-то эти флаги равны true, то у пользователя нет права использовать вводное предложение в пределах этой подписки, а также для всех подписок из этой же группы. Для определения всех подписок группы можно использовать поле subscription_group_identifier внутри responseBody.Pending_renewal_info.

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

Introductory offer доступен не для всех пользователей:

  • Для новых пользователей всегда доступно вводное предложение.

  • Для отписавшихся и не использовавших подобную скидку раньше также все доступно, но только при возобновлении подписки или активации любой другой подписки в пределах группы подписок.

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

Промо предложение — promotional offer

Как работает promotional offer

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

Для каждой подписки можно завести не более десяти промо-предложений. Пользователи, активировавшие introductory offer, также могут использовать и  promotional offer.

Решение, как и когда показывать промо предложение пользователю, ложится исключительно на плечи владельца приложения. Сценариев применения множество и они ничем не ограничиваются, например: 

  • предложить скидку, как только пользователь не обновил подписку или отменил ее;

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

Подготовка promotional offer и интеграции на стороне сервера

Добавление промо-предложений состоит из нескольких шагов, в частности:

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

  • показ промо на экране оплаты;

  • генерация подписи для совершения покупки с использованием промо;

  • покупка с использованием идентификатора промо.

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

Для начала нужно сгенерировать приватный ключ, которым сервер будет подписывать все активируемые промо-предложения. Для этого нужно пойти в App Store connect → Users and Access → Keys → In-App Purchase → Generate In-App Purchase Key → Ввести желаемое имя (оно используется лишь для отображения в списке) → Generate. После генерации приватного ключа требуется скачать его. Для скачивания он будет доступен только один раз, так что лучше его не терять.

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

Следующим шагом нужно добавить желаемые предложения в дашборде App Store Connect. Для каждого из них требуется указать:

  1. название (reference name), используется только для отображения в списке;

  2. идентификатор предложения (promotional offer id), активно используется в приложении при определении того, какую скидку требуется применить;

  3. метод предоставления скидки (offer type): free trial, pay as you go, pay upfront;

  4. продолжительность;

  5. цены, можно указывать разные цены для разных валют.

Клиентская реализация

Теперь можно приступить к реализации на стороне приложения.

Как и в случае с вводным предложением (introductory offer), нужно получить список продуктов из магазина, используя SKProductsRequest. У каждого продукта внутри поля discounts лежат все сгенерированные промо предложения для каждой конкретной подписки.

Владелец приложения сам регулирует то, какое промо предложение применить в той или иной ситуации. Делается это посредством идентификатора скидки — он используется для поиска нужного элемента из общего списка.

extension PaywallViewController: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        // your products are available here
        // let’s take a very first one as an example
        print("promo offers list: \(response.products.first!.discounts)")
        let myOffer = response.products.first?.discounts.filter { $0.identifier == "my_offer" }.first
    }    
}

Адаптация платежного экрана, чтобы вывести на нем информацию о промо-предложении, точно такая же, как и в случае с вводным предложением. Т.к. модель сущности SKProductDiscount используется та же самая, можно с легкостью переиспользовать код, который был написан для обработки introductory offer. Требуется лишь дополнительно передавать идентификатор предложения, который нужно применить:

extension ViewController {
    func formatPromoOfferMessage(for product: SKProduct, offerId: String) {
        let promoOffer = product.discounts.filter { $0.identifier == offerId }.first 
        let locale = product.priceLocale
        
        let localizedPrice = promoOffer?.price.localizedPrice(for: locale)
        self.discountLabel.text = localizedPrice // ex.: $10 or $9.99
        
        let localizedSubscriptionPeriod = promoOffer?.subscriptionPeriod.localizedPeriod(for: locale)
        discountDurationLabel.text = localizedSubscriptionPeriod // ex.: 3 weeks
    }
}

При покупке требуется явно указывать идентификатор предложения, который нужно применить, а также нужно получить сгенерированную подпись и еще ряд параметров для промо предложения с сервера. Далее передать это все в объект SKPaymentDiscount и уже этот объект скидки положить в поле paymentDiscount в покупку.

@available(iOS 12.2, *)
    private func createPayment(from product: SKProduct, offerId: String, completion: BuyProductCompletion? = nil) {
        // generate signing for promo offer
        ApiManager.signSubscriptionOffer(params: ["product": product.productIdentifier, "offer_id": offerId]) { (params, error) in
            guard error == nil else {
                // response error
                completion?(nil, nil, nil, product, error)
                return
            }
            
            guard
                let keyIdentifier = params?["key_id"] as? String,
                let nonceString = params?["nonce"] as? String,
                let nonce = UUID(uuidString: nonceString),
                let signature = params?["signature"] as? String,
                let timestampString = params?["timestamp"] as? String,
                let timestampInt64 = Int64(timestampString)
            else {
                // missing some of the offer params
                completion?(nil, nil, nil, product, Error.missingOfferSigningParams)
                return
            }
            
            let timestamp = NSNumber(value: timestampInt64)
            let payment = SKMutablePayment(product: product)
            payment.applicationUsername = ""
            payment.paymentDiscount = SKPaymentDiscount(identifier: offerId, keyIdentifier: keyIdentifier, nonce: nonce, signature: signature, timestamp: timestamp)
            
            SKPaymentQueue.default().add(payment)
        }
    }

При желании можно узнать, какое промо предложение было применено к конкретной покупке. Для этого надо посмотреть успешную транзакцию SKPaymentTransaction. Внутри нее есть поле paymentDiscount, где лежит идентификатор примененного промо.

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach { (transaction) in
            switch transaction.transactionState {
            case .purchased:
                print(transaction.payment.paymentDiscount?.identifier)
            ...
            }
        }
    }

Проверка доступности promo offer

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

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

  2. Далее нужно уже самостоятельно определять любые дополнительные критерии доступности конкретного предложения. Право на применение может зависеть от широкого спектра логики в зависимости от потребностей бизнеса.

Один из распространенных способов применения промо-предложений — через серверное уведомление DID_CHANGE_RENEWAL_STATUS, которое отправляется, когда пользователь меняет тип автопродления подписки. Например, когда пользователь отключает автопродление, можно показать ему промо-предложение, чтобы мотивировать возобновить подписку.

Также стоит учитывать, что промо-предложения доступны только на iOS 12.2 и выше, macOS 10.14.4 и выше, tvOS 12.2 и выше, поэтому нужно проверять доступность устройства и предлагать пользователю обновить OS, если он хочет применить промо предложение на неподдерживаемой версии операционной системы.

Промокоды — offer codes

Как работают offer codes

Промокоды — это строка из букв и цифр, которая даёт скидку на подписку или бесплатный триал. Промокоды стали доступны с iOS 14 и iPadOS 14, когда их анонсировали Apple. Распространять их можно как онлайн, так и оффлайн, например:

  • при отправке рассылки с новыми функциями приложения можно приложить промокод на бесплатный период, чтобы заинтересовать пользователя;

  • раздача листовок на улице с промокодом на скидку на первую покупку внутри приложения;

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

Реализация offer codes

Для начала нужно сгенерировать эти промокоды в пределах какой-то подписки. Делается это через App Store Connect. 

Пользователь может активировать код любым из трех способов:

  • Через App Store. Для этого нужно открыть App Store и в разделе с приложениями найти соответствующую форму ввода кода.

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

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

    SKPaymentQueue.default().presentCodeRedemptionSheet()

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

extension YourClass: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach { (transaction) in
            switch transaction.transactionState {
            case .purchased:
                // handle successful purchase
             
            case .failed:
                // handle error
                
            case .restored:
                // handle restore
             
            case .deferred, .purchasing: break
            @unknown default: break
            }
        }
    }
}

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

Для определения активированного промокода требуется проверять рецепт на наличие транзакции с полем offer_code_ref_name.  Это поле содержит название скидочного предложения, которое было создано в App Store Connect. Искать его следует в responseBody.Latest_receipt_info и responseBody.Pending_renewal_info внутри рецепта, но для серверных уведомлений аналогичные поля называются уже по-другому – unified_receipt.Latest_receipt_info и unified_receipt.Pending_renewal_info.

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

Ограничения

Можно сгенерировать только 10 скидочных предложений, для которых есть лимит в 150 000 промокодов за квартал на приложение, так что используйте их с умом. Срок жизни промокодов — шесть месяцев. Тестировать промокоды можно только на живой сборке из App Store, Sandbox для подобных вещей пока не предусмотрен.

Когда Apple ввели запрет на общий доступ к IDFA, некоторые пытались использовать промо-коды, чтобы атрибуцировать пользователей, но ограничение в 150 тысяч кодов свели все эти старания на нет.

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

Про Adapty

Adapty SDK упрощает подключение покупок в приложениях, поддерживает все актуальные фичи магазинов, как те, которые мы описали в этой статье, и делает серверную валидацию платежей. Но это не все преимущества:

  • Встроенная аналитика позволяет легко понимать главные метрики приложения.

  • Когортный анализ показывает, сходится ли экономика.

  • А/Б тесты пейволлов делаются налету и помогают увеличивать конверсию в платных пользователей.

  • Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики.

  • Промо кампании уменьшают отток аудитории.

  • Open source SDK позволяет интегрировать подписки в приложение за несколько часов.

  • Серверная валидация и API для работы с другими платформами упрощает работу с покупками.

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

Tags:
Hubs:
+1
Comments0

Articles

Information

Website
adapty.io
Registered
Founded
Employees
11–30 employees
Location
США