
В нашей прошлой статье мы рассказали о новом фреймворке 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.
Создание ключа в App Store ConnectДля создания ключа перейдите в:
Создайте ключ с любым именем и скачайте его. Внимание! Ключ можно скачать только один раз.
Для запросов нам понадобится так же Issuer ID, который можно получить из вкладки Keys > App Store Connect API. Если данное поле отсутствует, то нужно создать App Store Connect API Key, но не использовать его. Либо попробуйте зайти из-под владельца аккаунта.
Issuer ID в App Store ConnectДля создания 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, остальные запросы выполняются аналогично.
Следите за новостями!