Apphud – integrate, analyze and improve in-app purchases and subscriptions in your iOS/Android apps

Разработка

App Store Server API в действии

В нашей прошлой статье мы рассказали о новом фреймворке 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.

Для создания ключа перейдите в:

  1. Users and Access
  2. Keys
  3. In-App Purchase

Создайте ключ с любым именем и скачайте его. Внимание! Ключ можно скачать только один раз.

Issuer ID

Для запросов нам понадобится так же Issuer ID, который можно получить из вкладки Keys > App Store Connect API. Если данное поле отсутствует, то нужно создать App Store Connect API Key, но не использовать его. Либо попробуйте зайти из-под владельца аккаунта.

Создание токена для запросов

Для создания JWT токена используется стандарт RFC 7519, который описывает способ безопасной передачи данных.

Создание токена происходит в 3 этапа:

  1. Создание JWT хедера.
  2. Создание тела JWT.
  3. Подпись JWT.

Хедер состоит из трех полей и формируется очень просто:

{
"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
  • Shared Secret также больше не нужен.
  • Доступны новые поля, такие как status, type.
  • Доступны новые API, такие как управление возвратами из приложения.
  • Транзакции уже отсортированы на стороне Apple.

К недостаткам можно отнести:

  • Достаточно сложная авторизация запросов: необходимо генерировать API ключ и копировать Issuer ID из App Store Connect.
  • По-прежнему отсутствуют цены транзакций. Однако Apphud умеет правильно вычислять цены любых транзакций, даже в таких сложных случаях, как частичный возврат при апгрейдах, повышении цен у текущей подписки и др.

Мы рассмотрели лишь один запрос из нового App Store Server API, остальные запросы выполняются аналогично. Apphud уже приступил к работе с новым API и в скором времени будет выпущена обновленная версия Apphud SDK с поддержкой StoreKit2 и нового API.

Следите за новостями!

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

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