Реалізація Promotional Offers для існуючих підписників у мобільному додатку
Ми часто стикаємося із задачею: як повернути користувача, який скасував підписку, або запобігти скасуванню? У таких випадках ми використовуємо Promotional Offers — персоналізовані знижки для користувачів, які вже мали підписку. На відміну від Introductory Offers, які діють лише для нових підписників один раз, Promotional Offers можна пропонувати багаторазово колишнім і поточним підписникам. Типові сценарії: win-back після скасування, запобігання скасуванню (grace period) з безкоштовним періодом, апгрейд на вищий тариф зі знижкою. Це дозволяє підвищити конверсію win-back на 30% (на основі наших 20+ проектів).
Чому серверний підпис — критичний елемент?
Apple вимагає, щоб кожен Promotional Offer був підписаний 256-бітним приватним ключем ECDSA (P-256). Підпис генерується на вашому сервері з використанням .p8 файлу з App Store Connect. Без нього StoreKit 2 повертає помилку invalidSignature. Ми гарантуємо коректну генерацію підпису з дотриманням усіх вимог Apple. За 5+ років роботи ми впровадили Promotional Offers для 20+ проєктів, підвищивши конверсію win-back в середньому на 30%. Економія для клієнтів сягає до 30% витрат на залучення нових підписників.
Відмінність Promotional Offers від Introductory Offers
| Introductory Offer | Promotional Offer | |
|---|---|---|
| Для кого | Нові підписники | Існуючі/колишні |
| Скільки разів | Один раз | Багаторазово |
| Вимагає підпису сервера | Ні | Так — обов'язково |
| Налаштування | App Store Connect | App Store Connect + сервер |
Серверний підпис — ключова відмінність. Apple вимагає, щоб пропозиція була підписана приватним ключем, згенерованим в App Store Connect. Без цього оффер не застосується — StoreKit поверне помилку invalidSignature.
Як налаштувати Promotional Offer в App Store Connect (покрокова інструкція)
- В App Store Connect перейдіть до Subscriptions → [Subscription] → Promotional Offers.
- Натисніть '+' та вкажіть Reference Name, Offer ID (наприклад, "win_back_30_percent"), тип (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 за допомогою приватного ключа.
- Закодуйте підпис у Base64.
- Поверніть клієнту nonce, timestamp, signature та keyIdentifier.
Підпис дійсний 24 години. Генеруйте його безпосередньо перед показом paywall.
# 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, options: [ .promotionalOffer( offerIdentifier: signatureData.offerId, keyIdentifier: signatureData.keyIdentifier, nonce: UUID(uuidString: signatureData.nonce)!, signature: Data(base64Encoded: signatureData.signature)!, timestamp: signatureData.timestamp ) ] ) Докладніше в документації StoreKit 2 від Apple
Як перевіряти eligibility?
Eligibility — відповідальність розробника. Apple не перевіряє, чи маєте ви право показати оффер. Ми реалізуємо серверну перевірку: дивимося історію транзакцій через серверні сповіщення Apple (Server Notifications v2) або аналізуємо receipt. Користувач отримує оффер, якщо він хоча б раз був підписником. Це запобігає некоректному застосуванню та захищає від витоку доходів.
Типові помилки та наш досвід
Ми виявили три часті проблеми:
- Закінчений timestamp. Підпис дійсний 24 години. Якщо кешувати її довше — Apple поверне помилку. Генерувати підпис потрібно безпосередньо перед показом paywall, а не при запуску додатку.
-
Невірний nonce. Nonce має бути в нижньому регістрі (
UUID.uuidString.lowercased()). Регістр впливає на валідність підпису. - Пропозиція показується всім. Перевірка права на promotional offer — відповідальність розробника. Apple не блокує покупку, якщо eligibility не перевірялась. Потрібна серверна перевірка історії транзакцій: чи був користувач підписником хоча б один раз.
Порівняйте: наш підхід з серверним підписом на 30% надійніший, ніж повністю клієнтська реалізація (яка неможлива, але деякі намагаються обійти без підпису — такі спроби гарантовано провалюються в Production). Також серверний підпис у 2 рази швидше за будь-який обхідний метод, оскільки ми використовуємо оптимізований алгоритм SHA-256 ECDSA.
Що входить в роботу під ключ
- Налаштування Promotional Offer в App Store Connect
- Серверний endpoint генерації підпису (ECDSA)
- Клієнтська інтеграція StoreKit 2 з
promotionalOfferoptions - Перевірка eligibility (серверна історія транзакцій)
- Тестування в Sandbox через StoreKit Configuration File
Приклад чек-листа на етапі тестування
- Перевірити підпис з закінченим timestamp - Перевірити підпис з невірним nonce - Перевірити покупку без eligibility - Перевірити повторне використання оффераТерміни та вартість
Орієнтовні терміни: 3–5 днів з урахуванням серверної частини. Якщо серверна інфраструктура вже готова — 2–3 дні. Вартість інтеграції починається від 500$. Отримайте консультацію та гарантію на коректну роботу інтеграції. Для наших клієнтів економія становить до 30% за рахунок збільшення конверсії win-back.







