1 post tagged

introductory offer

Определяем триал правильно

Недавно в нашем приложении случился интересный кейс с in-app покупками — настолько неожиданный, что мы сначала решили, что это баг в StoreKit. А потом оказалось — всё работает ровно так, как должно. Просто знать об этом негде.

Мы делаем приложение, которое помогает пользователям в США подготовиться к сдаче на права — на автомобиль, мотоцикл, коммерческий транспорт. В процессе они проходят серию экзаменов и марафонов вопросов, и после успешного завершения получают сертификат, который нужен для подачи документов в DMV (аналог ГАИ в Штатах). Мы предлагаем доступ к приложению по подписке — и в ней недавно начали эксперимент с триалом на 3 дня, чтобы улучшить конверсию.

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

И вот через неделю дней после запуска триала нам начали писать в поддержку: «Я оплатил подписку, а сертификат не доступен». Сначала мы долго копали в сторону синхронизации прогресса, потом и восе потерялись в догадках. Как всегда «у меня точно такая же рука и не болит». Мы смотрим — действительно, в чеке Apple оплата прошла, но в приложении числится триал, как так может быть? На этом месте мы начали думать, что StoreKit барахлит, или где-то подвис кеш. Но таких пользователей стало больше. И все они присылали реальные чеки с оплатой. Удивительно!

Неделю копали и раскопали:

  • Пользователи действительно проходили все тесты за 1–2 дня — до окончания trial.
  • Видели, что сертификат недоступен, и не хотели ждать.
  • Шли в настройки приложения → Управление подписками → выбирали другую подписку из той же группы, но без триала — и оформляли её.
  • У обеих подписок был один и тот же groupID, и обе находились в currentEntitlements.

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

То есть у нас в логике было примерно так (очень упрощенно):


extension Transaction {
    /// Триальная ли транзакция
    var isTrial: Bool {
        guard 
                let expirationDate = expirationDate, 
                Date() < expirationDate 
        else { return false }
        return offer?.paymentMode == .freeTrial
    }

    /// Есть ли триальная транзакция у пользователя
    static var hasTrialSubscription: Bool {
        get async {
            for await result in Transaction.currentEntitlements {
                do {
                    if try result.payloadValue.isTrial {
                        return true
                    }
                } catch { continue }
            }
            return false
        }
    }
}


☝🏼

При смене одной подписки через настройки на другую (в той же группе) во время действия триала, появляется две активные транзакции: первая на время действия триала и вторая с оплаченной подпиской, с одинаковым originalTransactionId.



extension Transaction {
    /// Триальная ли транзакция
    var isTrial: Bool {
        guard let expirationDate = expirationDate, Date() < expirationDate else { return false }
        return offer?.paymentMode == .freeTrial
    }

    /// Есть ли триальная транзакция у пользователя
    static var hasTrialSubscription: Bool {
        get async {
            for await result in Transaction.currentEntitlements {
                do {
                    if try result.payloadValue.isTrial {
                        return true
                    }
                } catch { continue }
            }
            return false
        }
    }
}

Решение

Решение очень простое — нужно искать последнюю транзакцию с одним originalID и уже у неё проверять статус оффера.


extension Transaction {
    /// Есть ли триальная транзакция у пользователя
    static var hasTrialSubscription: Bool {
        get async {
            var latestTransactions: [UInt64: StoreKit.Transaction] = [:]
            for await result in Transaction.currentEntitlements {
                do {
                    let transaction = try result.payloadValue
                    if
                        let latestTransaction = latestTransactions[transaction.originalID],
                        transaction.purchaseDate < latestTransaction.purchaseDate
                    {  continue }
                    latestTransactions[transaction.originalID] = transaction
                } catch { continue }
            }
            return latestTransactions.values.contains(where: { $0.isTrial })
        }
    }
}

Вывод еще раз

StoreKit не инвалидирует первую триальную транзакцию из entitlements, даже если пользователь сменил подписку внутри группы. И это не баг, а особенность. Просто её надо учитывать, особенно если вы ограничиваете функциональность на trial-период.

Заметка в духе «запиши, чтобы потом не искать». Надеюсь, кому-то сэкономит пару часов и несколько тикетов в поддержку.

Буду рад вашей подписке на мой мой телеграм канал!

 No comments   21 h   In-App Purchases   introductory offer   iOS   offer   storekit   trial