Что нового в API StoreKit 2 и как он упростил интеграцию покупок в приложении

от автора

На недавно прошедшем 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 отлично решает эти вопросы:

  • Встроенная аналитика позволяет быстро понять основные метрики приложения.

  • Когортный анализ отвечает на вопрос, как быстро сходится экономика.

  • А/Б тесты увеличивают выручку приложения.

  • Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики.

  • Промо кампании уменьшают отток аудитории.

  • Open source SDK позволяет интегрировать подписки в приложение за несколько часов.

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

ссылка на оригинал статьи https://habr.com/ru/company/adapty/blog/565442/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *