В нашей прошлой статье мы рассказали о новом фреймворке StoreKit 2, представленном на WWDC 2021. Теперь мы расскажем об изменениях REST API, связанных со встроенными покупками.
App Store Server API – это новый REST API, позволяющий запрашивать информацию о встроенных покупках пользователя. Главным отличием от старого verifyReceipt
эндпоинта является то, что больше не нужно отправлять большой base64 чек. Для получения информации о покупках достаточно передать original transaction id, а авторизация происходит через API Key, сгенерированный в App Store Connect.
Создание ключа аналогично созданию ключа подписки. То есть вкладка Subscription Key просто переименована в In-App Purchase Key.
Для создания ключа перейдите в:
Создайте ключ с любым именем и скачайте его. Внимание! Ключ можно скачать только один раз.
Для запросов нам понадобится так же Issuer ID, который можно получить из вкладки Keys > App Store Connect API. Если данное поле отсутствует, то нужно создать App Store Connect API Key, но не использовать его. Либо попробуйте зайти из-под владельца аккаунта.
Для создания JWT токена используется стандарт RFC 7519, который описывает способ безопасной передачи данных.
Создание токена происходит в 3 этапа:
Хедер состоит из трех полей и формируется очень просто:
{ "alg": "ES256", "kid": "2X9R4HXF34", "typ": "JWT" }
Где alg
и typ
– статичные значения, а kid
– это ID ключа.
Основное тело JWT выглядит так:
{ "iss": "57246542-96fe-1a63e053-0824d011072a", "iat": 1623085200, "exp": 1623086400, "aud": "appstoreconnect-v1", "nonce": "6edffe66-b482-11eb-8529-0242ac130003", "bid": "com.apphud" }
Где
iss
– это Issuer ID, который мы получили из App Store Connect.
iat
– дата создания токена, в секундах.
exp
–дата истечения токена, в секундах. Не может быть больше чем через 1 час после даты создания токена.
aud
– фиксированное значение "appstoreconnect-v1".
nonce
– произвольная uuid строка, "соль".
bid
– Bundle ID приложения.
Более подробно о генерации токена можно почитать здесь.
Для получения получения списка транзакций необходим original transaction id подписки. По умолчанию API отдает 20 транзакций, отсортированных от старых к новым. Если имеется более 20 транзакций, то параметр hasMore
вернетtrue
.
https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{original_transaction_id}
В sandbox окружении URL имеет другой домен:
https://api.storekit-sandbox.itunes.apple.com
Библиотека JWT очень популярна и есть для всех основных языков. Рассмотрим на примере Ruby
. Создадим класс StoreKit
:
require 'jwt' require_relative 'jwt_helper' require 'httparty' class StoreKit ... attr_reader :private_key, :issuer_id, :original_transaction_id, :key_id, :bundle_id, :response ALGORITHM = 'ES256' def jwt JWT.encode( payload, private_key, ALGORITHM, headers ) end def headers { kid: key_id, typ: 'JWT' } end def payload { iss: issuer_id, iat: timestamp, exp: timestamp(1800), aud: 'appstoreconnect-v1', nonce: SecureRandom.uuid, bid: bundle_id } end end
Здесь мы объявили методы, которые формируют хедер и тело JWT.
Добавим в наш класс StoreKit переменную URL и код выше для запроса информации о транзакции:
URL = 'https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/%<original_transaction_id>s' def request! url = format(URL, original_transaction_id: original_transaction_id) result = HTTP.get(url, headers: { 'Authorization' => "Bearer #{jwt}" }) # raise UnauthenticatedError if result.code == 401 # raise ForbiddenError if result.code == 403 result.parsed_response end
И вызываем сам класс StoreKit
в отдельном файле subscription.rb
:
key_id = File.basename(ENV['KEY'], File.extname(ENV['KEY'])).split('_').last ENV['KEY_ID'] = key_id StoreKit.new( private_key: File.read("#{Dir.pwd}/keys/#{ENV['KEY']}"), issuer_id: '69a6de82-48b4-47e3-e053-5b8c7c11a4d1', original_transaction_id: ENV['OTI'], key_id: key_id, bundle_id: 'com.apphud' ).call
Выполнив запрос, мы получаем массив ответ, который так же подписан с помощью JWT.
Для декодирования ответа понадобится Public Key, который можно получить из скачанного нами Private Key. Напишем небольшой хелпер JWTHelper
:
require 'jwt' require 'byebug' require 'openssl/x509/spki' # JWT class class JWTHelper ALGORITHM = 'ES256' def self.decode(token) JWT.decode(token, key, false, algorithm: ALGORITHM).first end def self.key OpenSSL::PKey.read(File.read(File.join(Dir.pwd, 'keys', ENV['KEY']))).to_spki.to_key end end
Данный хелпер читает приватный ключ с помощью библиотеки OpenSLL и извлекает публичный ключ с помощью метода to_spki
(Simple Public Key Infrastructure).
Далее декодируем наш ответ:
def decoded_response response['data'].each do |item| item['lastTransactions'].each do |t| t['signedTransactionInfo'] = JWTHelper.decode(t['signedTransactionInfo']) t['signedRenewalInfo'] = JWTHelper.decode(t['signedRenewalInfo']) end end response end
Если все прошло верно, то получим конечный JSON:
{ "environment": "Sandbox", "bundleId": "com.apphud", "data": [ { "subscriptionGroupIdentifier": "20771176", "lastTransactions": [ { "originalTransactionId": "1000000809414960", "status": 2, "signedTransactionInfo": { "transactionId": "1000000811162893", "originalTransactionId": "1000000809414960", "webOrderLineItemId": "1000000062388288", "bundleId": "com.apphud", "productId": "com.apphud.monthly", "subscriptionGroupIdentifier": "20771176", "purchaseDate": 1620741004000, "originalPurchaseDate": 1620311199000, "expiresDate": 1620741304000, "quantity": 1, "type": "Auto-Renewable Subscription", "inAppOwnershipType": "PURCHASED", "signedDate": 1623773050102 }, "signedRenewalInfo": { "expirationIntent": 1, "originalTransactionId": "1000000809414960", "autoRenewProductId": "com.apphud.monthly", "productId": "com.apphud.monthly", "autoRenewStatus": 0, "isInBillingRetryPeriod": false, "signedDate": 1623773050102 } } ] } ] }
Как видно, в массиве lastTransactions
присутствует информация о последней транзакции подписки, а также статус подписки = `2`, что означает expired
. Все статусы подписки описаны здесь.
Из нового так же появилось поле "type": "Auto-Renewable Subscription"
, которое отдает тип покупки в читабельном виде.
К сожалению, цены транзакций по-прежнему недоступны через API.
Исходный код из данной статьи доступен по этой ссылке.
Новый App Store Server API предоставляет больше информации для разработчиков и будет работать значительно быстрее засчет отсутствия в параметрах больших base64 ресиптов.
К преимуществам можно отнести:
original_transaction_id
.К недостаткам можно отнести:
Мы рассмотрели лишь один запрос из нового App Store Server API, остальные запросы выполняются аналогично.
Следите за новостями!