Платные приложения, за которые надо заплатить авансом, могут быть сложными для продвижения в App Store. Страница вашего продукта может получать много просмотров, но если эти просмотры не приводят к загрузкам, что-то нужно менять. Именно в такой ситуации я оказался с Maxine: приличный трафик, почти никаких продаж.
Поэтому я перешёл на модель freemium, хотя на самом деле этого не хотел. В итоге данные оказались довольно очевидными, и я получаю отзывы и от других разработчиков. Бесплатные загрузки с дополнительными встроенными покупками обеспечивают лучшую конверсию и привлекают больше пользователей. После размышлений о наилучшем способе перехода я решил, что существующие пользователи получают пожизненный бесплатный доступ, а новые пользователи получают 5 тренировок, прежде чем им потребуется оформить подписку или разблокировать пожизненную подписку. Это должно дать им достаточно времени, чтобы как следует протестировать приложение, прежде чем они решат его купить.
В этом посте мы рассмотрим следующие темы:
- Как сохранить доступ для существующих пользователей, используя данные StoreKit
- Как обойти возможные проблемы при тестировании
- Последовательность релизов, обеспечивающая плавный переход
В конце вы узнаете, как перевести своё платное приложение на модель freemium, не оставляя своих лояльных первых пользователей.
Предоставление пользователям доступа к приложению через StoreKit
Независимо от способа реализации встроенных покупок, вы можете использовать StoreKit для проверки даты первой установки вашего приложения пользователем. Это позволяет идентифицировать пользователей, которые оплатили приложение до того, как оно стало бесплатным, и автоматически предоставить им пожизненный доступ.
Это можно сделать с помощью AppTransaction API в StoreKit. Он предоставляет доступ к исходной версии приложения и дате первоначальной покупки для текущего устройства. Это довольно хороший способ определить пользователей, которые приобрели ваше приложение до перехода на модель freemium.
Вот как проверить первую установленную версию (что я и сделал для Максин):
import StoreKit
func isLegacyPaidUser() async -> Bool {
do {
let appTransaction = try await AppTransaction.shared
switch appTransaction {
case .verified(let transaction):
// The version string from the first install
let originalVersion = transaction.originalAppVersion
// Compare against your last paid version
// For example, if version 2.0 was your first free release
if let version = Double(originalVersion), version < 2.0 {
return true
}
return false
case .unverified:
// Transaction couldn't be verified, treat as new user
return false
}
} catch {
// No transaction available
return false
}
}
Поскольку такая логика потенциально может привести к потере дохода, я настоятельно рекомендую написать несколько модульных тестов, чтобы убедиться, что ваши проверки на устаревшие версии работают должным образом. Мой подход к тестированию проверки на устаревшие версии заключался в создании метода, который брал бы строку версии из AppTransaction и проверял её на соответствие целевой версии. Таким образом, я знаю, что мой тест надёжен. Я также позаботился о тестах, например, о проверке того, что пользователи, отмеченные как Pro из-за нумерации версий, проходят все проверки, выполняемые в моём вспомогательном методе ProAccess. Например, проверяя, разрешено ли им начинать новую тренировку.
Я выбрал проверку версии, но вы также можете использовать дату первоначальной покупки, если это лучше подходит для вашей ситуации:
import StoreKit
func isLegacyPaidUser(cutoffDate: Date) async -> Bool {
do {
let appTransaction = try await AppTransaction.shared
switch appTransaction {
case .verified(let transaction):
// When the user first installed (purchased) the app
let originalPurchaseDate = transaction.originalPurchaseDate
// If they installed before your freemium launch date, they're legacy
return originalPurchaseDate < cutoffDate
case .unverified:
return false
}
} catch {
return false
}
}
// Usage: check if installed before your freemium release
let isLegacy = await isLegacyPaidUser(
cutoffDate: DateComponents(
calendar: .current,
year: 2026,
month: 1,
day: 30
).date!
)
Опять же, если вы решите внедрить подобное решение, я настоятельно рекомендую добавить модульные тесты, чтобы избежать ошибок, которые могут привести к потере дохода.
Версионный подход хорошо работает, когда у вас есть четкие границы версий. Подход с использованием дат полезен, если вы не уверены, какой номер версии будет выпущен, или если вам нужна большая гибкость.
После того, как вы определили статус пользователя, вам нужно будет сохранить его локально, чтобы не проверять квитанцию каждый раз:
import StoreKit
actor EntitlementManager {
static let shared = EntitlementManager()
private let defaults = UserDefaults.standard
private let legacyUserKey = "isLegacyProUser"
var hasLifetimeAccess: Bool {
defaults.bool(forKey: legacyUserKey)
}
func checkAndCacheLegacyStatus() async {
// Only check if we haven't already determined status
guard !defaults.bool(forKey: legacyUserKey) else { return }
let isLegacy = await isLegacyPaidUser()
if isLegacy {
defaults.set(true, forKey: legacyUserKey)
}
}
private func isLegacyPaidUser() async -> Bool {
do {
let appTransaction = try await AppTransaction.shared
switch appTransaction {
case .verified(let transaction):
if let version = Double(transaction.originalAppVersion), version < 2.0 {
return true
}
return false
case .unverified:
return false
}
} catch {
return false
}
}
}
Моё приложение предназначено для одного устройства, поэтому мне не нужно беспокоиться о сценариях с несколькими устройствами. Если ваше приложение синхронизирует данные между устройствами, вам может понадобиться более сложное решение. Например, вы можете хранить метку «legacy pro» в CloudKit или на вашем сервере, чтобы права доступа следовали за учётной записью iCloud пользователя, а не были привязаны к одному устройству.
Кроме того, хранение в UserDefaults — это несколько наивный подход. В зависимости от минимальной версии вашей ОС, вы можете запускать приложение в среде, потенциально взломанной (jailbroken); это позволит пользователям довольно легко изменять UserDefaults, и гораздо безопаснее хранить эту информацию в keychain или проверять чек каждый раз. Для простоты в этом посте я использую UserDefaults, но я рекомендую вам провести надлежащую оценку рисков безопасности, чтобы определить, какой подход подойдёт именно вам.
С этим кодом вы готовы начать тестирование…
Подводные камни тестирования
Тестирование чеков имеет некоторые особенности, о которых вам следует знать, прежде чем выпускать приложение.
TestFlight всегда сообщает версию 1.0
Когда ваше приложение запускается через TestFlight, оно работает в изолированной среде, и AppTransaction.originalAppVersion возвращает «1.0» независимо от того, какую сборку фактически установил тестировщик. Это делает невозможным тестирование логики на основе версий только через TestFlight.
Вы можете обойти это, используя отладочные сборки с ручным переключением, позволяющим имитировать работу старых пользователей. Добавьте скрытое отладочное меню или используйте аргументы запуска, чтобы переопределить проверку на устаревание во время разработки.
#if DEBUG
var debugOverrideLegacyUser: Bool? = nil
#endif
func isLegacyPaidUser() async -> Bool {
#if DEBUG
if let override = debugOverrideLegacyUser {
return override
}
#endif
// Normal receipt-based check...
}
Переустановка сбрасывает исходную версию
Если пользователь удаляет и переустанавливает ваше приложение, originalAppVersion отражает версию, которую он переустановил, а не самую первую установку. Это ограничение проверки чека на устройстве. Если вы записали статус Pro пользователя в keychain, вы сможете получить статус Pro оттуда.
К сожалению, я не нашел надежного способа обойти переустановку и сброс чеков. Для моего приложения это приемлемо. У меня не так много пользователей, поэтому я думаю, что риск потери кем-то доступа к старой версии Pro будет минимальным.
Манипулирование временем устройства
Пользователи с неправильным временем устройства могут обойти ваши проверки на основе даты. Именно поэтому я выбрал проверку на основе версии, но опять же, все дело в определении того, какой риск приемлем для вас и вашего приложения.
Переход
Когда вы будете готовы к релизу, последовательность действий имеет значение. Вот что я сделал:
1. Установите для вашего приложения ручной выпуск. В App Store Connect настройте ручное, а не автоматическое обновление новой версии. Это позволит вам контролировать сроки.
2. Добавьте примечание для проверки приложения. В примечаниях для рецензента объясните, что вы измените цену приложения на бесплатную перед выпуском. Например: «Это обновление переводит приложение из платной версии в условно-бесплатную. Я изменю цену на бесплатную в App Store Connect перед выпуском этой версии, чтобы обеспечить плавный переход для пользователей».
3. Дождитесь одобрения. Пусть проверка приложения одобрит вашу сборку, пока она технически еще является платным приложением.
4. Сначала сделайте приложение бесплатным. После одобрения перейдите в App Store Connect и измените цену приложения на бесплатную (или настройте уровни цен условно-бесплатной версии).
5. Затем выпустите. После того, как изменение цены вступит в силу, вручную выпустите одобренную сборку.
Я не на 100% уверен, что порядок имеет значение, но сделать приложение бесплатным перед выпуском показалось мне самым безопасным подходом. Это гарантирует, что в момент загрузки новой условно-бесплатной версии пользователи случайно не будут платить за старую платную версию.
Вкратце
Сохранение прав платных пользователей при переходе на модель freemium сводится к проверке AppTransaction на предмет исходной версии или даты установки. Кэшируйте результат локально и рассмотрите CloudKit или серверное хранилище, если вам нужны права доступа для разных устройств.
Тестирование — сложная задача, поскольку TestFlight всегда сообщает версию 1.0, а чеки в песочнице не всегда точно соответствуют производственной версии. Используйте переключатели отладки и, в идеале, реальное устройство со старой версией приложения из App Store для тщательного тестирования.
При релизе установите ручной релиз сборки, добавьте примечание для проверки приложения, объясняющее переход, а затем сделайте приложение бесплатным, прежде чем нажать кнопку релиза.
Изменение стратегии монетизации может показаться признанием поражения, но на самом деле это всего лишь итерация. App Store — конкурентный рынок, ожидания пользователей меняются, и то, что работало при запуске, может перестать работать через шесть месяцев. Обращайте внимание на данные о конверсии, будьте готовы адаптироваться и не позволяйте мышлению о невозвратных затратах удерживать вас в модели, которая не служит ни вашим пользователям, ни вашему бизнесу.

