На недавно прошедшем WWDC 2021 представили новую версию StoreKit 2. Это фреймворк, который отвечает за осуществление покупок в iOS. Доля приложений со встроенными покупками и подписками постоянно растёт, и выпустив StoreKit 2, Apple заметно упростил интеграцию покупок в приложение. Сегодня мы рассмотрим работу с StoreKit 2 со стороны сервера, то есть с помощью App Store Server API.
Аутентификация запросов
В текущей версии API для отправки запроса необходим Shared Secret. Это секретная фиксированная строка, которую можно получить в App Store Connect. В новой версии API для аутентификации запросов используется стандарт JSON Web Token (JWT).
Генерация ключа
Прежде всего необходимо создать приватный ключ, с помощью которого будут подписываться запросы. Это делается в App Store Connect в разделе Users and Access. Там необходимо перейти на вкладку Keys и выбрать тип ключа In-App Purchase. После создания ключ необходимо скачать. Вам также понадобится его ID, который можно скопировать на этой же странице, и Issuer ID, который находится на вкладке App Store Connect API.
Создание токена
Следующим шагом надо создать токен, которым будут подписываться запросы. Этот процесс в деталях описан в документации, поэтому не буду подробно на нём останавливаться, а просто приведу пример готовой реализации на Python. Отмечу, что не стоит генерить новый токен для каждого запроса. При создании токена вы можете указать время его жизни до 60 минут и использовать один и тот же токен в течение этого периода.
Получение JWT-токена для работы с App Store Server API
import time, uuid
from authlib.jose import jwt
BUNDLE_ID = 'com.adapty.sample_app'
ISSUER_ID = '4336a124-f214-4d40-883b-6db275b5e4aa'
KEY_ID = 'J65UYBDA74'
PRIVATE_KEY = '''
-----BEGIN PRIVATE KEY-----
MIGTAgMGByqGSMBHkAQQgR/fR+3Lkg4...
-----END PRIVATE KEY-----
'''
issue_time = round(time.time())
expiration_time = issue_time + 60 * 60 # 1 hour expiration
header = {
'alg': 'ES256',
'kid': KEY_ID,
'typ': 'JWT'
}
payload = {
'iss': ISSUER_ID,
'iat': issue_time,
'exp': expiration_time,
'aud': 'appstoreconnect-v1',
'nonce': str(uuid.uuid4()),
'bid': BUNDLE_ID
}
token_encoded = jwt.encode(header, payload, PRIVATE_KEY)
token_decoded = token_encoded.decode()
authorization_header = {
'Authorization': f'Bearer {token_decoded}'
}
Подписанные транзакции (signed transactions)
В новой версии API все транзакции возвращаются в стандарте JSON Web Signature (JWS). Это строка из трёх частей, которые разбиты точками.
Base64 хэдер.
Base64 пейлоад транзакции.
Подпись транзакции.
Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))
Хэдер транзакции
Хэдер нужен для того, чтобы подтвердить подлинность транзакции. В ключе alg хранится алгоритм шифрования, в ключе x5c — цепочка сертификатов.
Хэдер транзакции
{
"kid": "AMP/DEV",
"alg": "ES256",
"x5c": [
"MIIEO...",
"MIIDK..."
]
}
Пейлоад транзакции
Apple изменила и расширила формат транзакций, на мой взгляд, с ними теперь удобней работать. Подробно о новом формате можно прочитать в документации, ниже я опишу самые важные изменения.
Добавили поле
appAccountToken
, которое содержит идентификатор пользователя в вашей системе. Этот идентификатор должен быть в формате UUID и задаётся на стороне мобильного приложения в момент инициализации покупки. Если задан, то он будет возвращаться во всех транзакциях в этой цепочке (продления, проблемы с биллингом и тд.), а значит вы легко сможете понять, какой пользователь сделал покупку.-
Добавили поля
offerType
иofferIdentifier
, которые содержат информацию об используемом оффере, если он есть. Возможные значения для поляofferType
:1
— интро оффер (доступен только для пользователей без активных или истёкших подписок)2
— промо оффер (доступен только для существующих или истёкших подписчиков)3
— оффер код
Если был использован промо оффер или оффер код (значение
2
или3
), то в ключеofferIdentifier
придёт идентификатор использованного оффера. Ранее не было возможности отследить использование оффер кодов со стороны сервера, и это портило аналитику. Теперь же оффер коды можно полноценно учитывать в аналитике.-
Добавили поле
inAppOwnershipType
, с помощью которого можно понять, купил ли пользователь продукт сам или получил его в рамках семейной подписки. Возможные значения:PURCHASED
FAMILY_SHARED
-
Добавили поле
type
, в котором хранится тип транзакции. Возможные значения:Auto-Renewable Subscription
Non-Consumable
Consumable
Non-Renewing Subscription
Поля
cancellation_date
иcancellation_reason
переименовали вrevocationDate
иrevocationReason
. Напомню, что эти поля содержат дату и причину аннулирования подписки в результате рефанда, так что новое название выглядит более логично.Все ключи возвращаются в camelCase (как и во всех запросах App Store Server API).
Все даты возвращаются в формате Unix timestamp в миллисекундах.
Пейлоад транзакции
{
"transactionId": "1000000831360853",
"originalTransactionId": "1000000806937552",
"webOrderLineItemId": "1000000063561721",
"bundleId": "com.adapty.sample_app",
"productId": "basic_subscription_1_month",
"subscriptionGroupIdentifier": "27636320",
"purchaseDate": 1624446341000,
"originalPurchaseDate": 1619686337000,
"expiresDate": 1624446641000,
"quantity": 1,
"type": "Auto-Renewable Subscription",
"appAccountToken": "fd12746f-2d3a-46c8-bff8-55b75ed06aca",
"inAppOwnershipType": "PURCHASED",
"signedDate": 1624446484882,
"offerType": 2,
"offerIdentifier": "basic_subscription_1_month.pay_as_you_go.3_months"
}
Статус подписки пользователя
Для получения актуального статуса подписки пользователя необходимо отправить GET запрос на https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}
, где {originalTransactionId}
— это идентификатор любой цепочки транзакций пользователя. В ответ придёт массив транзакций со статусами для каждой группы подписок.
Ответ на запрос о статусе подписки пользователя
{
"environment": "Sandbox",
"bundleId": "com.adapty.sample_app",
"data": [
{
"subscriptionGroupIdentifier": "39636320",
"lastTransactions": [
{
"originalTransactionId": "1000000819078552",
"status": 2,
"signedTransactionInfo": "eyJraWQiOi...",
"signedRenewalInfo": "eyJraWQiOi..."
}
]
}
]
}
В ключе status
хранится текущее состояние подписки, на него нужно ориентироваться, чтобы понять, давать ли пользователю доступ к платным функциям приложения. Возможные значения:
1
— подписка активна, пользователю должны быть доступны платные функции.2
— подписка истекла, пользователю должны быть недоступны платные функции.3
— подписка находится в статусе Billing Retry, то есть пользователь её не отменял, но произошла проблема с оплатой. Apple будет пытаться списать деньги в течение 60 дней. Пользователю должны быть недоступны платные функции.4
— подписка находится в статусе Grace Period, то есть пользователь её не отменял, но произошла проблема с оплатой. При этом в App Store Connect включен Grace Period, значит пользователю должны быть доступны платные функции.5
— подписка аннулирована в результате рефанда, пользователю должны быть недоступны платные функции.
В ключе signedTransactionInfo
хранится информация о последней транзакции в цепочке в формате, который мы подробно разбирали выше.
Информация о продлении подписки
В ключе signedRenewalInfo
хранится информация о продлении подписки.
Эта информация позволяет понимать, что произойдёт с подпиской в следующем периоде. Например, если вы обнаружили, что пользователь отменил автопродление, вы можете предложить ему переход на другой план или промо оффер. Отслеживать такие события удобно с помощью серверных уведомлений, о которых я скоро расскажу.
Информация о продлении подписки
{
"expirationIntent": 1,
"originalTransactionId": "1000000819078552",
"autoRenewProductId": "basic_subscription_1_month",
"productId": "basic_subscription_1_month",
"autoRenewStatus": 0,
"isInBillingRetryPeriod": false,
"signedDate": 1624520884048
}
История транзакций пользователя
Для получения истории транзакций пользователя, необходимо отправить GET запрос на https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}
, где {originalTransactionId}
— это идентификатор любой цепочки транзакций пользователя. В ответ придёт массив транзакций, отсортированных в хронологической последовательности.
В запросе максимально возвращается 20 транзакций, если у пользователя их больше, то значение флага hasMore
будет true
. Если вы хотите получить следующую страницу транзакций, то необходимо повторить запрос с GET-параметром revision
, в котором будет значение из такого же ключа в ответе.
Ответ на запрос об истории транзакций пользователя
{
"revision": "1625872984000_1000000212854038",
"bundleId": "com.adapty.sample_app",
"environment": "Sandbox",
"hasMore": true,
"signedTransactions": [
"eyJraWQiOiJ...",
"joiRVMyNeyX...",
"5MnkvOTlOZl...",
...
]
}
Серверные уведомления о транзакциях
С помощью серверных уведомлений можно получать информацию о новых покупках, продлениях, проблемах с платежами и так далее. Это позволяет собирать более точную аналитику, а также упрощает менеджмент состояния подписчика.
Существующие серверные уведомления (V1) решают большую часть задач, но в некоторых ситуациях работа с ними не очень удобная. В основном это касается ситуаций, когда на одно действие пользователя приходит несколько уведомлений. Например, сейчас при апргрейде подписки Apple присылает два события: DID_CHANGE_RENEWAL_STATUS
и INTERACTIVE_RENEWAL
, чтобы корректно обработать этот кейс, нужно как-то хранить состояние, проверять, не пришло ли второе событие. В новой версии серверных уведомлений (V2), на одно действие пользователя всегда будет приходить только одно событие, это значительно удобней.
Во второй версии серверных уведомлений появились новые события OFFER_REDEEMED
, EXPIRED
и GRACE_PERIOD_EXPIRED
, которые сильно упрощают управление состоянием подписчика. События SUBSCRIBED
и PRICE_INCREASE
— это улучшенные события из первой версии.
Серверные уведомления содержат информацию о транзакции и продление в уже знакомом нам формате JWS.
Подтипы уведомлений
В уведомлениях появились подтипы, именно благодаря ним теперь на любое действие пользователя достаточно одного уведомления, чтобы понять, что произошло.
Серверное уведомление
{
"notificationType": "SUBSCRIBED",
"subtype": "INITIAL_BUY",
"version": 2,
"data": {
"environment": "Sandbox",
"bundleId": "com.adapty.sample_app",
"appAppleId": 739104078,
"bundleVersion": 1,
"signedTransactionInfo": "eyJraWQiOi...",
"signedRenewalInfo": "eyJraWQiOi..."
}
}
Работа в Sandbox окружении
Для того, чтобы тестировать покупки, необходимо использовать URL Sandbox окружения: https://api.storekit-sandbox.itunes.apple.com
.
Новая версия серверных уведомлений пока недоступна для тестирования, но когда она появится, будет возможность указать разные URL для приёма Production и Sandbox уведомлений. Для Sandbox можно будет выбрать V2, а на Production на время отладки оставить V1.
Также в App Store Connect теперь можно:
Очищать историю покупок для Sandbox пользователя, то есть для этого не нужно создавать новый аккаунт.
Изменять страну стора Sandbox пользователя.
Изменять периодичность продления Sandbox подписок. Например, можно сделать так, что ежемесячная покупка будет длиться 1 час, а не 5 минут, как было раньше.
Заключение
Apple значительно улучшил работу со встроенными покупками и подписками со стороны сервера. На мой взгляд, самые полезные нововведения:
полноценная поддержка промо офферов и кодов;
более информативные и простые серверные уведомления;
возможность узнать актуальный статус подписки без вычисления состояния;
очистка истории покупок Sandbox пользователя.
Переход на новый API не должен быть очень сложным, достаточно будет для каждого receipt получить originalTransactionId. Вполне вероятно, что он уже хранится у вас в базе.
Про Adapty
В любом случае, самая сложная часть в интеграции подписок в мобильное приложение — это построение системы аналитики и оптимизация экономики. Adapty отлично решает эти вопросы:
Встроенная аналитика позволяет быстро понять основные метрики приложения.
Когортный анализ отвечает на вопрос, как быстро сходится экономика.
А/Б тесты увеличивают выручку приложения.
Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики.
Промо кампании уменьшают отток аудитории.
Open source SDK позволяет интегрировать подписки в приложение за несколько часов.
Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии.
MoneyZmey
Спасибо огромное, ведь apple это отдельная головная боль