На недавно прошедшем 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.

Создание приватного ключа для работы с App Store Server API
Создание приватного ключа для работы с App Store Server 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). Это строка из трёх частей, которые разбиты точками.

  1. Base64 хэдер.

  2. Base64 пейлоад транзакции.

  3. Подпись транзакции.

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 позволяет интегрировать подписки в приложение за несколько часов.

Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии.

Комментарии (1)


  1. MoneyZmey
    05.08.2021 14:35
    +1

    Спасибо огромное, ведь apple это отдельная головная боль