What’s new: Integrations Update, Connection Builder, Custom Parameters in Rule’s Push Payloads, and more.Let’s see
Apphud
Why Apphud?PricingContact
Ren
Ren
June 18, 2021
2 min read

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

В этой статье мы расскажем об изменениях App Store REST API, связанных со встроенными покупками.

2 min read
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.

Создание ключа в App Store ConnectСоздание ключа в App Store Connect

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

  • Users and Access
  • Keys
  • In-App Purchase

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

Issuer ID

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

Issuer ID в App Store ConnectIssuer ID в App Store Connect

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

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

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

  • Создание JWT хедера.
  • Создание тела JWT.
  • Подпись 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 приложения.

Более подробно о генерации токена можно почитать здесь.

Get transaction information

Для получения получения списка транзакций необходим 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, остальные запросы выполняются аналогично.

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

Ren
Ren
Co-founder at Apphud
Ex iOS app and game developer. 11 years in the industry since iOS 3. More than 50 apps are in the background with 4 exits. Entrepreneur and traveler.

Related Posts

By clicking on the «Allow all» button, you accept the use of cookies on this website. We use cookies to measure and analyze our traffic as well as for marketing purposes (e.g. for retargeting ads). You can edit your preferences at any time. For more information, refer to our privacy policy.