Реализация экрана управления подпиской в мобильном приложении
Экран управления подпиской — это не просто страница с кнопкой «Отменить». Это точка, где пользователь принимает решение остаться или уйти. Неправильно реализованный экран ускоряет отмены: пользователь не понимает, что именно он теряет, когда истечёт доступ и можно ли получить скидку.
Данные, которые нужно показать
Минимальный набор:
- Текущий тариф (название, цена, период)
- Дата следующего списания или дата истечения при отмене
- Статус: активна / отменена (но доступ до) / grace period / billing retry
- Кнопка перехода на другой тариф (upgrade/downgrade)
- Ссылка на системный экран Apple/Google для отмены
Чтение текущего состояния (StoreKit 2)
import StoreKit
struct SubscriptionInfo {
let productID: String
let displayName: String
let price: String
let renewalDate: Date?
let expirationDate: Date?
let willAutoRenew: Bool
let state: Product.SubscriptionInfo.RenewalState
let isInGracePeriod: Bool
}
func loadSubscriptionInfo(productIds: [String]) async -> SubscriptionInfo? {
guard let product = try? await Product.products(for: productIds).first,
let status = try? await product.subscription?.status.first else {
return nil
}
let renewalInfo = try? status.renewalInfo.payloadValue
let transaction = try? status.transaction.payloadValue
return SubscriptionInfo(
productID: product.id,
displayName: product.displayName,
price: product.displayPrice,
renewalDate: renewalInfo?.renewalDate,
expirationDate: transaction?.expirationDate,
willAutoRenew: renewalInfo?.willAutoRenew ?? false,
state: status.state,
isInGracePeriod: status.state == .inGracePeriod
)
}
renewalInfo.willAutoRenew — показывает, включено ли автопродление. Если пользователь отменил подписку, это поле будет false, но доступ активен до expirationDate.
SwiftUI-компонент экрана
struct SubscriptionManagementView: View {
@StateObject private var viewModel = SubscriptionManagementViewModel()
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
if let info = viewModel.subscriptionInfo {
CurrentPlanCard(info: info)
if info.state == .inGracePeriod {
GracePeriodWarning()
}
if info.willAutoRenew, let date = info.renewalDate {
Text("Следующее списание: \(date.formatted(.dateTime.day().month().year()))")
.foregroundStyle(.secondary)
} else if let date = info.expirationDate {
Text("Доступ активен до: \(date.formatted(.dateTime.day().month().year()))")
.foregroundStyle(.secondary)
}
PlanPickerSection(currentPlan: info.productID)
}
ManageButton()
}
.padding()
}
.task { await viewModel.load() }
}
}
Системная кнопка управления подпиской (обязательна)
Apple требует, чтобы в приложении был доступ к системному экрану управления подписками. Без этого — rejection по guideline 3.1.2:
Button("Управление подпиской в App Store") {
Task {
try? await AppStore.showManageSubscriptions(in: windowScene)
}
}
На Android — deeplink в Google Play:
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("https://play.google.com/store/account/subscriptions?sku=premium_monthly&package=${packageName}")
}
startActivity(intent)
Cancellation flow — не просто кнопка
Лучшие практики удержания при попытке отменить:
- Перед уходом на системный экран — показать что теряет пользователь (список фич)
- Если подписка активна давно — предложить паузу вместо отмены (Google Play поддерживает нативно)
- Для пользователей с давней историей — предложить Promotional Offer со скидкой
Это не dark pattern — это честное напоминание о ценности. Важно не злоупотреблять: один экран подтверждения максимум.
Что входит в работу
- Чтение статуса через StoreKit 2 / Google Play Billing Library
- Компонент отображения: тариф, дата, статус, grace period
- Переключатель тарифов (upgrade/downgrade) с описанием условий
- Интеграция
AppStore.showManageSubscriptions/ Play Store deeplink - Опциональный cancellation flow с retention-оффером
Сроки
2–3 дня для базового экрана с актуальным статусом. Со сложным cancellation flow и retention-офферами — до 5 дней. Стоимость рассчитывается индивидуально.







