What’s new in Apphud: Paywall Analytics, Stripe Web Payments Support and Xiaomi SDKLet’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