Реалізація Promotional Offers для існуючих абонентів у мобільних застосунках
Promotional Offers — персоналізовані знижки для користувачів, які вже були абонентами. Introductory Offer можна запропонувати тільки один раз і тільки новому абоненту. Promotional Offer — повторно і тільки тим, хто вже мав або має активну підписку. Типові сценарії: повернути користувача, що відмінив підписку; запобігти відміні через win-back оффер; перейти на більш високий тариф зі знижкою.
Різниця від Introductory Offers
| Introductory Offer | Promotional Offer | |
|---|---|---|
| Для кого | Нові абоненти | Існуючі/колишні |
| Скільки разів | Один раз | Багато разів |
| Вимагає серверний підпис | Ні | Так — обов'язково |
| Налаштування | App Store Connect | App Store Connect + сервер |
Серверний підпис — ключова відмінність. Apple вимагає, щоб оффер був підписаний приватним ключем, згенерованим в App Store Connect. Без цього оффер не застосується — StoreKit повернеться помилку invalidSignature.
Налаштування в App Store Connect
- Subscriptions → [Subscription] → Promotional Offers →
+ - Встановлюємо Reference Name, Offer ID, тип (freeTrial / payAsYouGo / payUpFront), тривалість та ціну
- Зберігаємо Offer ID — він знадобиться при генерації підпису
Паралельно: Keys → Subscription Key → створюємо ключ, завантажуємо .p8 файл та запам'ятовуємо Key ID.
Серверний підпис
Сервер генерує підпис за алгоритмом ECDSA з ключем .p8. Параметри:
-
appBundleId— bundle ID застосунку -
keyIdentifier— Key ID з App Store Connect -
productIdentifier— ID продукту -
offerIdentifier— Offer ID -
applicationUsername— ID користувача у вашій системі (опційно, але рекомендується) -
nonce— UUID, генерується сервером -
timestamp— поточний час у мілісекундах
Підпис створюється з конкатенації цих значень через \n, підписується SHA-256 ECDSA:
# Приклад Python для сервера (спрощено)
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
import base64, uuid, time
def generate_signature(bundle_id, key_id, product_id, offer_id, username):
nonce = str(uuid.uuid4()).lower()
timestamp = str(int(time.time() * 1000))
message = "\n".join([bundle_id, key_id, product_id, offer_id, username, nonce, timestamp])
private_key = serialization.load_pem_private_key(PRIVATE_KEY_PEM, password=None)
signature = private_key.sign(message.encode(), ec.ECDSA(hashes.SHA256()))
encoded = base64.b64encode(signature).decode()
return {"nonce": nonce, "timestamp": timestamp, "signature": encoded, "keyIdentifier": key_id}
Сервер повертає ці дані клієнту; клієнт використовує їх при підтвердженні покупки.
Застосування на клієнті (StoreKit 2)
import StoreKit
// Отримуємо параметри підпису з сервера
let signatureData = try await apiClient.fetchPromoOfferSignature(
productId: "premium_monthly",
offerId: "win_back_30_percent"
)
// Отримуємо продукт
guard let product = try? await Product.products(for: ["premium_monthly"]).first else { return }
// Знаходимо оффер за ID
guard let offer = product.subscription?.promotionalOffers.first(where: {
$0.id == "win_back_30_percent"
}) else { return }
// Створюємо об'єкт підписаного оффера
let signedOffer = try await offer.purchase(
confirmIn: self, // WindowScene або UIViewController
options: [
.promotionalOffer(
offerIdentifier: signatureData.offerId,
keyIdentifier: signatureData.keyIdentifier,
nonce: UUID(uuidString: signatureData.nonce)!,
signature: Data(base64Encoded: signatureData.signature)!,
timestamp: signatureData.timestamp
)
]
)
Типові помилки
Прострочений timestamp. Підпис дійсний 24 години. Якщо його кешувати довше — Apple повернеться помилку. Генерувати підпис потрібно безпосередньо перед показом paywall, а не при старті застосунку.
Невірний nonce. Nonce має бути в нижньому регістрі (UUID.uuidString.lowercased()). Регістр впливає на дійсність підпису.
Оффер показується всім. Перевірка права на promotional offer — відповідальність розробника. Apple не блокує покупку, якщо eligibility не була перевірена. Потрібна серверна перевірка історії транзакцій: був ли користувач абонентом хоча б один раз?
Що входить у роботу
- Налаштування Promotional Offer в App Store Connect
- Серверний endpoint генерації підпису (ECDSA)
- Клієнтська інтеграція StoreKit 2 з
promotionalOfferопціями - Перевірка eligibility (серверна історія транзакцій)
- Тестування у Sandbox через StoreKit Configuration File
Терміни
3–5 днів — з урахуванням серверної частини (генерація підпису). Якщо серверна інфраструктура вже готова — 2–3 дні. Вартість розраховується індивідуально.







