На недавно прошедшем 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 отлично решает эти вопросы:
-
Встроенная аналитика позволяет быстро понять основные метрики приложения.
-
Когортный анализ отвечает на вопрос, как быстро сходится экономика.
-
А/Б тесты увеличивают выручку приложения.
-
Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики.
-
Промо кампании уменьшают отток аудитории.
-
Open source SDK позволяет интегрировать подписки в приложение за несколько часов.
Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии.
ссылка на оригинал статьи https://habr.com/ru/company/adapty/blog/565442/
Добавить комментарий