Pull to refresh

In-app Billing Subscriptions со стороны сервера

Reading time 8 min
Views 19K
In-app Billing Subscriptions (подписки) позволяют автоматизировать списание средств со счета пользователя для приложений, разработанных под Android. Данный инструмент — большой помощник в задаче повышения монетизации приложений. В общем виде схема работы с подписками выглядит следующим образом:

  1. Пользователь покупает подписку на некоторые плюшки приложения
  2. В случае успешной покупки, приложение получает данные заказа пользователя, в частности идентификатор транзакции и токен продажи подписки, и передает их на сервер
  3. Сервер осуществляет проверку подписи заказа в Google Play, контролирует уникальность транзакции, определяет время завершения подписки и начисляет положенные блага
  4. По завершении подписки, сервер может определить факт продления и, в случае успеха, продолжить начисление благ


В статье представлены шаги по обеспечению серверной поддержки инструмента монетизации для In-App Billing version 2.


Для получения данных по подпискам со стороны сервера, Google предлагает использовать Google Play Android Developer API. API предоставляет всего два метода:

  • Получение данных по подписке
  • Отмена подписки. Ну как отмена… на самом деле отмена автоматической пролонгации подписки. Ибо по-справедливости весь период действия подписки оплачен наперед


Если попробовать получить данные по некоторой подписке myapp.month.test приложения com.myapp (кстати, все совпадения представленных ниже токенов, продуктов, идентификаторов и ключей случайны. Для повторения примеров прошу использовать данные своего аккаунта), результат будет следующим:

$ wget 'https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription'
--2013-02-11 18:24:01--  https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription
Resolving www.googleapis.com (www.googleapis.com)... 173.194.71.95, 2a00:1450:4010:c04::5f
Connecting to www.googleapis.com (www.googleapis.com)|173.194.71.95|:443... connected.
HTTP request sent, awaiting response... 401 Unauthorized
Authorization failed.


Для успешного вызова методов работы с подписками необходим токен доступа к API. Последний можно получить, выполнив авторизацию по протоколу OAuth2 с использованием секретных данных консоли. Идентифицироваться при авторизации можно различными способами. Важно, что для получения токена доступа к методам взаимодействия с подписками, нужно представиться веб-приложением, используя данные раздела Client ID for web applications:

image

Авторизация веб-приложением осуществляется в следующей последовательности
  1. Открыть в браузере ссылку accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher
    &response_type=code&access_type=offline
    &redirect_uri=http://example.com/oauth2callback
    &client_id=someclientid.apps.googleusercontent.com
    Здесь кроме прочего, обратите внимание на access type offline, польза которого станет очевидна позже
  2. Осуществить логин
  3. Разрешить аккаунту доступ к запрошенным ресурсам на экране, подобном представленому

    image
  4. Запомнить код из обратной ссылки example.com/oauth2callback?code=4/AuthoRIZ4ti0nC0De
  5. Отправить запрос авторизации

    $ wget https://accounts.google.com/o/oauth2/token --post-data 'code=4/AuthoRIZ4ti0nC0De&client_id=someclientid.apps.googleusercontent.com&client_secret=c1iEnT5eCReT&redirect_uri=http://example.com/oauth2callback&grant_type=authorization_code' -O -
    --2013-02-11 18:31:20--  https://accounts.google.com/o/oauth2/token
    Resolving accounts.google.com (accounts.google.com)... 173.194.71.84, 2a00:1450:4010:c04::54
    Connecting to accounts.google.com (accounts.google.com)|173.194.71.84|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: unspecified [application/json]
    Saving to: `STDOUT'
    
        [<=>                                                                                                                                                                                          ] 0           --.-K/s              {
      "access_token" : "ya29.ACCeSs-T0keN",
      "token_type" : "Bearer",
      "expires_in" : 3600,
      "refresh_token" : "1/rEfRE5hT0KeN"
        [ <=>                                                                                                                                                                                         ] 127         --.-K/s   in 0s      
    
    2013-02-11 18:31:20 (16.6 MB/s) - written to stdout [127]
    


  6. В поле access_token представлен токен доступа к API. Он предоставляет возможность получать данные по подпискам в течение 3600 секунд, как указано в expires_in. По истечении этого времени токен, к сожалению, перестанет действовать
  7. Если вдруг по каким-то причинам (ну мало-ли, забыли) токен доступа понадобится ещё раз, можно получить авторизационный код повторно и повторить запрос.

    $ wget https://accounts.google.com/o/oauth2/token --post-data 'code=4/0thER-AuthoRIZ4ti0nC0De&client_id=someclientid.apps.googleusercontent.com&client_secret=c1iEnT5eCReT&redirect_uri=http://example.com/oauth2callback&grant_type=authorization_code' -O -
    --2013-02-11 18:31:20--  https://accounts.google.com/o/oauth2/token
    Resolving accounts.google.com (accounts.google.com)... 173.194.71.84, 2a00:1450:4010:c04::54
    Connecting to accounts.google.com (accounts.google.com)|173.194.71.84|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: unspecified [application/json]
    Saving to: `STDOUT'
    
        [<=>                                                                                                                                                                                          ] 0           --.-K/s              {
      "access_token" : "ya29.neW-ACCeSs-T0keN",
      "token_type" : "Bearer",
      "expires_in" : 3573,
        [ <=>                                                                                                                                                                                         ] 127         --.-K/s   in 0s      
    
    2013-02-11 18:31:20 (16.6 MB/s) - written to stdout [127]
    




В результате Google предоставляет новый токен доступа, но без токена обновления. Все полученные таким образом токены являются валидными для выполнения запросов, но обновить их будет невозможно. Чтобы как можно реже выполнять процедуру, описанную выше, и хоть как-то автоматизировать процесс проверки подписок рекомендую предпринять следующие действия:

  1. Сохранить токен доступа в какое-нибудь хранилище
  2. Сохранить в максимально надежном месте токен обновления токена доступа. Кстати, он-то нам и доступен балгодаря access type offline. Если время действия кода обратной ссылки ограничено, про ограничение токена обновления данных нет. На моей практике сам по себе, он пока не завершился ни разу
  3. При устаревании токена доступа, о чем можно узнать по уже знакомому ответу с характерным кодом HTTP 401 на вызовы методов подписок, необходимо отправить запрос обновления токена доступа следующего содержания

    $ wget 'https://accounts.google.com/o/oauth2/token' --post-data 'refresh_token=1/rEfRE5hT0KeN&client_id=someclientid.apps.googleusercontent.com&client_secret=c1iEnT5eCReT&grant_type=refresh_token' -O -
    --2013-02-11 19:33:13--  https://accounts.google.com/o/oauth2/token
    Resolving accounts.google.com (accounts.google.com)... 173.194.71.84, 2a00:1450:4010:c04::54
    Connecting to accounts.google.com (accounts.google.com)|173.194.71.84|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: unspecified [application/json]
    Saving to: `STDOUT'
    
        [<=>                                                                                                                                                                                          ] 0           --.-K/s              {
      "access_token" : "ya29.rEFre5hED-ACCeSs-T0keN",
      "token_type" : "Bearer",
      "expires_in" : 3600
        [ <=>                                                                                                                                                                                         ] 127         --.-K/s   in 0s      
    
    2013-01-11 19:33:14 (18.7 MB/s) - written to stdout [127]
    


  4. Обновить токен доступа в том самом хранилище


Обеспечив себе получение свежих токенов при необходимости, можно смело проверять-отменять приобретенные платящими пользвоателями подписки. Пример получения данных о подписке:

$ wget 'https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription?access_token=ya29.rEFre5hED-ACCeSs-T0keN' -O -
--2013-02-11 19:50:21--  https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription?access_token=ya29.rEFre5hED-ACCeSs-T0keN
Resolving www.googleapis.com (www.googleapis.com)... 173.194.71.95, 2a00:1450:4010:c04::5f
Connecting to www.googleapis.com (www.googleapis.com)|173.194.71.95|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/json]
Saving to: `STDOUT'

    [<=>                                                                                                                                                                                          ] 0           --.-K/s              {
 "kind": "androidpublisher#subscriptionPurchase",
 "initiationTimestampMsec": "1357909784285",
 "validUntilTimestampMsec": "1360588184285",
 "autoRenewing": true
}
    [ <=>                                                                                                                                                                                         ] 167         --.-K/s   in 0s      

2013-02-11 19:50:21 (30.7 MB/s) - written to stdout [167]


Описание полей можно посомтреть в документации к запросу

Следует отметить расхождение значений initiationTimestampMsec и validUntilTimestampMsec:

  • initiationTimestampMsec = 1357909784.285 секунды = 2013-01-11 17:09:44
  • validUntilTimestampMsec = 1360588184.285 секунды = 2013-02-11 23:09:44


Полагаю, Google резервирует дополнительно 6 часов на списание средств со счета клиента. В случае возникновения проблем при списании, периодически пытается повторить. В конкретно этом случае по данным паблишмента деньги были списаны у клиента ~ в 2013-02-11 17:10. Кстати, подписки Facebook пытаются списать средства вплоть до 4 суток после завершения платежного периода. А вот iTunes начинает списывать деньги за сутки до окончания платежного периода так, чтобы к моменту завершения подписки было точно известно, продлена она или нет.

Если с данными запроса что-то не так, Google отвечает 400-м кодом HTTP. В некоторых случаях при некорректном запроса возникает HTTP 404.

$ wget 'https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/not.myapp.month.test/purchases/subsubsubscripscription?access_token=ya29.rEFre5hED-ACCeSs-T0keN' -O -
--2013-02-11 19:58:24--  https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/not.myapp.month.test/purchases/subsubsubscripscription?access_token=ya29.rEFre5hED-ACCeSs-T0keN
Resolving www.googleapis.com (www.googleapis.com)... 173.194.71.95, 2a00:1450:4010:c04::5f
Connecting to www.googleapis.com (www.googleapis.com)|173.194.71.95|:443... connected.
HTTP request sent, awaiting response... 400 Bad Request
2013-02-11 19:58:25 ERROR 400: Bad Request.


Пример отмены подписки:

$ wget 'https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription/cancel?access_token=ya29.rEFre5hED-ACCeSs-T0keN' --post-data '' -O -
--2013-02-11 20:01:16--  https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription/cancel?access_token=ya29.rEFre5hED-ACCeSs-T0keN
Resolving www.googleapis.com (www.googleapis.com)... 173.194.71.95, 2a00:1450:4010:c04::5f
Connecting to www.googleapis.com (www.googleapis.com)|173.194.71.95|:443... connected.
HTTP request sent, awaiting response... 204 No Content


Обратите внимание на код ответа HTTP 204, а пустое тело ответа свидетельсвут об успешной отмене подписки. Если повторить запрос получения данных по подписке, можно в этом убедиться

$ wget 'https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription?access_token=ya29.rEFre5hED-ACCeSs-T0keN' -O -
--2013-02-11 20:08:47--  https://www.googleapis.com/androidpublisher/v1/applications/com.myapp/subscriptions/myapp.month.test/purchases/subsubsubscripscription?access_token=ya29.rEFre5hED-ACCeSs-T0keN
Resolving www.googleapis.com (www.googleapis.com)... 173.194.71.95, 2a00:1450:4010:c04::5f
Connecting to www.googleapis.com (www.googleapis.com)|173.194.71.95|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/json]
Saving to: `STDOUT'

    [<=>                                                                                                                                                                                          ] 0           --.-K/s              {
 "kind": "androidpublisher#subscriptionPurchase",
 "initiationTimestampMsec": "1357909784285",
 "validUntilTimestampMsec": "1360588184285",
 "autoRenewing": false
}
    [ <=>                                                                                                                                                                                         ] 167         --.-K/s   in 0s      

2013-02-11 20:08:49 (23.5 MB/s) - written to stdout [167]


Обратите внимание, дата окончания подписки не изменилась, но автопролонгация оказалась выключенной.
Для интеграции подписок в приложение, предлагаю следующую архитектуру системы серверной поддержки:

image

Описание:

  • buy — покупка подписки на стороне клиента
  • verify — верификация данных подписки на стороне сервера, определение момента завершения подписки
  • queue — очередь верифицированных подписок
  • periodical verification — периодическая проверка подписок. Если подписка была продлена, ее можно записать обратно в очередь. Период проверки подписок надо выбирать таким образом, чтобы укладываться в лимит запросов к API. Для нашего приложения он составляет 15K/сутки
  • access token refreshing — блок обновления токена доступа, если он устарел


Если кто-нибудь соберется реализовывать подписки, предлагаю две библиотеки:

Tags:
Hubs:
+14
Comments 8
Comments Comments 8

Articles