Реализация Offer Codes для подписок в мобильном приложении
Offer Codes — это промокоды Apple для подписок. Позволяют выдавать бесплатный доступ или скидку без привязки к рекламной сети, без необходимости вводить данные карты. Типичные сценарии: партнёрские акции, коды для инфлюенсеров, оффлайн-распространение (QR на упаковке), корпоративные продажи.
Принципиальное отличие от Promotional Offers: Offer Code не требует серверной подписи и может быть активирован через системный диалог прямо в приложении или через ссылку https://apps.apple.com/redeem?ctx=offercodes&id=...&code=....
Создание кодов в App Store Connect
Subscriptions → [Subscription] → Offer Codes → +. Задаём Reference Name, Offer ID, тип скидки (freeTrial / payAsYouGo / payUpFront), длительность. Apple позволяет создавать одноразовые коды (One-Time Use) или многоразовые (Custom). Лимит одноразовых кодов — 150 000 в квартал на подписку.
Реализация диалога активации в приложении
StoreKit 2 предоставляет OfferCodeRedeemSheet — системный UI для ввода кода:
import StoreKit
import SwiftUI
struct SettingsView: View {
@State private var showRedeemSheet = false
var body: some View {
List {
Button("Ввести промокод") {
showRedeemSheet = true
}
}
.offerCodeRedemption(isPresented: $showRedeemSheet) { result in
switch result {
case .success(let transaction):
// Пользователь активировал код — обновляем UI
await updateSubscriptionStatus(transaction)
case .failure(let error):
handleRedemptionError(error)
case .pending:
// Транзакция в обработке
break
}
}
}
}
На UIKit (iOS 16+) аналог через AppStore.presentOfferCodeRedeemSheet(in:):
Task {
do {
try await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
} catch {
// Показываем fallback — ссылку на apps.apple.com/redeem
}
}
На iOS ниже 16 presentOfferCodeRedeemSheet недоступен. Fallback: открываем URL https://apps.apple.com/redeem?ctx=offercodes&id=APP_ID&code=CODE через UIApplication.open.
Обработка транзакций после активации
После успешной активации кода StoreKit генерирует транзакцию. Её нужно поймать через Transaction.updates:
// Запускается при старте приложения и слушает всё время
for await result in Transaction.updates {
if case .verified(let transaction) = result {
if transaction.offerType == .code {
// Активирован Offer Code — разблокируем контент
await unlockPremiumContent()
await transaction.finish()
}
}
}
Важно вызвать transaction.finish() — без него транзакция остаётся в pending-очереди и может повторно появиться при следующем запуске.
Android — Google Play Promo Codes
На Android аналог — Promo Codes в Google Play. Отличается архитектурой: коды создаются в Google Play Console, активируются через Play Store или deeplink. На стороне приложения через Billing Library 5+:
// Google Play уведомляет через PurchasesUpdatedListener
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
handleNewPurchase(purchase)
}
}
}
}
Типичные проблемы
Коды одноразового использования не имеют Preview в Sandbox до их фактического выпуска — тестирование требует создания Custom кода или sandbox-аккаунта с вручную применённым кодом через тестовую ссылку Apple.
Ещё одна проблема: если приложение не слушает Transaction.updates при холодном старте, а пользователь активировал код через web-ссылку — транзакция ждёт в очереди, но приложение не разблокирует контент до следующего явного вызова Transaction.currentEntitlements.
Что входит в работу
- Настройка Offer Code в App Store Connect
- Интеграция
OfferCodeRedeemSheet/AppStore.presentOfferCodeRedeemSheet - Fallback для iOS ниже 16
- Обработка транзакций через
Transaction.updates - Тестирование с Sandbox Offer Codes
Сроки
3–5 дней с учётом интеграции в существующий paywall и subscription-flow. Стоимость рассчитывается индивидуально.







