Всем привет, я Кирилл, СТО Adapty. Я делал систему серверной валидации для наших SDK и сегодня расскажу про то, как её настроить для приложений на Android.
Это пятая статья из серии о внедрении внутренних покупок на Android, советую познакомиться с остальными:
Android in-app purchases, часть 1: конфигурация и добавление в проект.
Android in-app purchases, часть 2: инициализация и обработка покупок.
Android in-app purchases, часть 3: получение активных покупок и смена подписки.
Android in-app purchases, часть 5: серверная валидация покупок. — Вы тут.
Что такое серверная валидация покупки?
Серверная валидация позволяет проверить подлинность покупки: устройство обращается к серверам Google за информацией, действительно ли была совершена покупка, и валидна ли она.
Зачем валидировать покупки
Серверная валидация необязательна, покупки будут работать без неё. Но с ней лучше, потому что она даёт важные преимущества:
Продвинутая аналитика платежей. Это особенно актуально для подписок. После того, как подписка активирована, списания происходят без участия устройства. Если пользователь не заходит в приложение, без серверной обработки мы не сможем своевременно получать информацию о статусе подписки: продлил, отменил, возникла проблема с платежом.
Проверка подлинности покупки. Серверная валидация позволяет убедиться, что транзакция действительно произошла, и покупка была не фродовой. Поскольку на Android процент мошеннических операций выше, чем на iOS, то в приложениях на Google Play об этом особенно важно позаботиться.
Кроссплатформенные подписки, имея данные по статусу подписки в живом режиме, можно синхронизировать подписки пользователя с другими платформами. Так, если пользователь подписался на приложение на Android, доступ к нему у него будет на iOS, Web и других платформах.
Возможность контролировать доступ к контенту со стороны сервера. Серверная валидация не даст доступ к контенту тем пользователям, которые хотят получить его, отправляя запросы к серверу.
Вообще уже один первый пункт стоит того, чтобы настроить серверную валидацию.
Валидация платежей
В целом процесс валидации платежей на Android можно описать схемой:
Аутентификация запросов к Google Play Developer API
Для того, чтобы работать с Google Play Developer API, необходимо создать ключ, с помощью которого будут подписываться все запросы. Сначала надо связать аккаунт Google Play Console (место, где вы управляете приложением) с аккаунтом в Google Cloud (там будет создавать ключ для подписи запросов). После того, как всё настроено, необходимо дать права пользователю на менеджмент покупок. Чтобы подробно описать весь этот процесс, понадобится отдельная статья. К счастью, мы уже сделали пошаговую инструкцию в документации Adapty, а к выходу этой статьи я обновил скриншоты, потому что интерфейс периодически меняется.
Обратите внимание, что обычно после создания ключа необходимо ждать сутки или больше, прежде чем он заработает. Чтобы этого не делать, можно обновить описание у любого in-app продукта или подписки, тогда ключ сразу будет активным.
Мы используем официальную библиотеку google-api-python-client для работы с Google Play Developer API. Данная библиотека доступна для большинства популярных языков, и я рекомендую использовать её, потому что в ней поддерживаются все нужные методы.
Валидация транзакций о подписке
В отличии от серверной валидации iOS, в Android валидация подписок и остальных продуктов делается с помощью разных методов. Поэтому в момент валидации вам необходимо понимать, работаете ли вы с подпиской или продуктом. На практике это означает, что вам надо передавать эту информацию со стороны мобильного приложения, а также хранить флаг в базе данных, если возникнет необходимость повторной проверки токена.
Второе большое отличие — в Android у каждой транзакции есть свой токен, в то время как на iOS все транзакции хранятся в одном токене (shared secret). Это означает, что для восстановления покупок пользователя необходимо хранить все токены покупок, а не только один для любой из них.
Для валидации подписки необходимо вызвать метод purchases.subscriptions.get. По факту это вызов GET-запроса https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
.
Все параметры обязательны:
packageName
— идентификатор приложения, например, com.adapty.sample_app.subscriptionId
— идентификатор подписки, которую нужно провалидировать, например, com.adapty.sample_app.weekly_sub.token
— уникальный токен транзакции, который появляется после покупки на стороне мобильного приложения.
Для начала разберём ошибки, которые необходимо отслеживать, чтобы всё корректно работало:
400, Invalid grant: account not found — ошибка означает, что неправильно создан ключ для аутентификации запросов. Убедитесь, что используется верный аккаунт, учётные записи связаны, есть достаточные права, а также активированы необходимые API. О том, как всё правильно настроить, написано в предыдущей секции. Также обратите внимание на подсказку про обновление описания продукта.
400, The purchase token does not match the package name — эта ошибка чаще всего встречается во фродовых транзакциях. Если вы видите её в процессе тестирования, убедитесь, что вы не отправляете токен от покупки в одном приложении в другое.
403, Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'androidpublisher.googleapis.com' — превышена квота на количество запросов к API Google в сутки. По умолчанию можно отправлять не более 200000 запросов в сутки. Эту квоту можно увеличить, но для большинства приложений её должно быть достаточно. Возможно, если вы упёрлись в неё, то стоит убедиться в правильности логики работы.
410, The subscription purchase is no longer available for query because it has been expired for too long — ошибка возвращается для транзакций, где подписка истекла более 60 дней назад. По сути это не ошибка, так что не стоит её обрабатывать как ошибку.
Подписочная транзакция
В случае успешной валидации в ответ приходит информация о транзакции.
Информация о транзакции (подписка)
{
"expiryTimeMillis": "1631116261362",
"paymentState": 1,
"acknowledgementState": 1,
"kind": "androidpublisher#subscriptionPurchase",
"orderId": "GPA.3382-9215-9042-70164",
"startTimeMillis": "1630504367892",
"autoRenewing": true,
"priceCurrencyCode": "USD",
"priceAmountMicros": "1990000",
"countryCode": "US",
"developerPayload": ""
}
Чтобы понимать, есть ли у пользователя доступ к платным функциям приложения, то есть имеется ли у него активная подписка, необходимо:
Проверить параметры
startTimeMillis
иexpiryTimeMillis
, текущее время должно быть между ними.Кроме того, необходимо убедиться, что параметр
paymentState
не равен1
. Это означает, что подписка ожидает оплаты и в настоящий момент не нужно давать доступ к платным функциям приложения.Если в транзакции присутствует поле
autoResumeTimeMillis
, то подписка на паузе, то есть доступ к платным функциям приложения не нужно предоставлять раньше этой даты.
Давайте разберём важные свойства подписочной транзакции:
kind
— тип транзакции, у подписок он всегдаandroidpublisher#subscriptionPurchase
. С помощью этого параметра можно определять, подписка перед вами или продукт, и в зависимости от этого выбирать логику обработки.paymentState
— статус платежа, не приходит для истёкших транзакций. Возможные значения:0
— по данной транзакции ещё не прошла оплата. В некоторых странах подписку можно оплатить в магазине, то есть сначала пользователь оформляет её на устройстве, потом идёт в терминал и оплачивает там. В целом, это редкий кейс, но стоит иметь в виду.1
— подписка оплачена.2
— подписка находится в триальном периоде.3
— в следующем периоде произойдёт апгрейд или даунгрейд текущей подписки, то есть смена тарифного плана.
acknowledgementState
— статус подтверждения покупки. Это важный параметр, который служит для подтверждения, получил ли пользователь доступ к тому, за что он платил. Значение0
означает, что не получил,1
— получил. Разработчик сам отвечает за простановку этого статуса, это можно сделать как на стороне мобильного приложения, так и на стороне сервера. Если не подтвердить статус покупки, то через 3 дня автоматически произойдёт рефанд. Рекомендую реализовать логику, что, если в транзакции пришёлacknowledgementState=0
, то сервер его изменяет. О том, как это сделать, расскажу ниже.orderId
— уникальный идентификатор транзакции. У каждой покупки/продления будет свой идентификатор, его можно использовать, чтобы понимать, была ли ранее обработана данная транзакция. У каждого продления основная часть идентификатора остаётся неизменной, просто в конце добавляется две точки и номер продления, начиная с 0. Если при активации подписки у неё был идентификаторGPA.3382-9215-9042-70164,
то у первого продления будетGPA.3382-9215-9042-70164..0
, у второго —GPA.3382-9215-9042-70164..1
и тд. Таким образом можно выстраивать цепочки транзакций и отслеживать количество продлений.startTimeMillis
— дата начала подписки.expiryTimeMillis
— дата окончания подписки.autoRenewing
— флаг, показывающий, будет ли продлена подписка на следующий период.priceCurrencyCode
— валюта покупки в 3-х символьном формате, например RUB.priceAmountMicros
— цена покупки. Для того, чтобы получить нормальное числовое значение цены, надо поделить данное значение на 1000000. То есть 1990000 — это 1.99.countryCode
— страна аккаунта, с которого совершена покупка, в 2-х символьном формате, например, RU.purchaseType
— тип покупки. В большинстве случаев этого ключа нет. Но при этом его важно учитывать, потому что с помощью него можно понимать, что покупка была совершена в Sandbox окружении. Возможные значения:0
— покупка из Sandbox окружения, то есть её не нужно учитывать в аналитике.1
— покупка оплачена промокодом.
autoResumeTimeMillis
— дата, когда подписка возобновится. Приходит только для подписок, которые были поставлены на паузу. Если этот параметр есть, то доступ к платным функциям приложения не нужно предоставлять раньше указанной даты.cancelReason
— причина, по которой подписка не будет продлеваться на следующий период. Возможные значения:0
— пользователь сам отменил автопродление.1
— подписка отменена системой, чаще всего это связано с billing issue, то есть с пользователя не смогли снять деньги.2
— пользователь перешёл на другую подписку.3
— разработчик отменил подписку.
userCancellationTimeMillis
— дата, когда пользователь отменил продление подписки. Приходит только еслиcancelReason=0
. Подписка может быть всё ещё активна, это надо смотреть по параметруexpiryTimeMillis
.cancelSurveyResult
— объект, в котором хранится информация о причине отмены подписки пользователем, если он ответил на данный вопрос.introductoryPriceInfo
— объект, в котором хранится информации о цене для вводного периода (специальное предложение, например 1 месяц со скидкой 50%).promotionType
— тип промокода, который был использован при активации подписки. Возможные значения:0
— одноразовый промокод.1
— кастомный промокод, который может быть использован несколькими пользователями. Такие коды можно использовать для рекламы у блогеров.
promotionCode
— кастомный промокод, который был использован при активации подписки. Данный параметр не приходит для одноразовых промокодов.priceChange
— объект, в котором хранится информация о будущем изменении цены, а также статус того, одобрил ли пользователь это изменение или нет.
Подтверждение подписки (acknowledge subscription)
Как говорилось выше, если не подтвердить подписку, то через 3 дня она будет автоматически отменена, и пользователю вернутся деньги. Если честно, я не понимаю, зачем нужна такая логика, ни в каких других системах оплаты, в том числе iOS, я её не встречал. Тем не менее, если в транзакции пришёл acknowledgementState=0
, необходимо подтвердить подписку.
Чтобы подтвердить подписку, нужно вызвать метод purchases.subscriptions.acknowledge. Данный метод делает POST-запрос https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}:acknowledge
. Параметры точно такие же, как при валидации запроса. В случае успешного выполнения запроса подписка будет подтверждена, а значит, вы не потеряете деньги.
Если подписка ещё не оплачена, то её не надо подтверждать.
Отмена возобновления, отзыв, рефанд и продление подписки
Кроме валидации и подтверждения подписки, с помощью Google Play Developer API можно делать ещё несколько операций с подписками. Стоит отметить, что все они довольно редкие, и за исключением продления, все поддерживаются в Google Play Console. Но всё же перечислю их, чтобы было общее представление о возможностях API. Во всех запросах такие же обязательные параметры, как и в ранее рассмотренных методах: packageName
, subscriptionId
и token
.
Отмена возобновления. Метод purchases.subscriptions.cancel. Отменяет продление для выбранной подписки, при этом подписка будет активна до окончания текущего периода.
Рефанд (возврат денег) подписки. Метод purchases.subscriptions.refund. Возвращает пользователю деньги за подписку. При этом доступ к подписке остаётся, и в следующем периоде она автоматически продлится. В большинстве случаев вместе с рефандом надо использовать отзыв подписки.
Отзыв подписки. Метод purchases.subscriptions.revoke. Моментально прекращает действие подписки, то есть у пользователя сразу же пропадает доступ к платным функциям приложения. Подписка не будет продлеваться в дальнейшем. Обычно используется вместе с рефандом.
Продление подписки. Метод purchases.subscriptions.defer. Продлевает действие подписки до указанной даты. В запросе необходимо передать текущую дату истечения подписки и желаемую, которая обязательно больше текущей.
Валидация продуктов (не подписок)
Валидация продуктов похожа на валидацию подписок. Необходимо вызвать метод purchases.products.get, который выполнит GET-запрос https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}
. Все параметры уже нам знакомы из рассмотренных ранее примеров.
Информация о транзакции (продукт)
{
"purchaseTimeMillis": "1630529397125",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "",
"orderId": "GPA.3374-2691-3583-90384",
"acknowledgementState": 1,
"kind": "androidpublisher#productPurchase",
"regionCode": "RU"
}
Транзакция продукта содержит заметно меньше значений, чем транзакция подписки. Рассмотрим важные свойства:
kind
— тип транзакции, у продуктов он всегда androidpublisher#productPurchase. С помощью этого параметра можно определять, подписка перед вами или продукт, и в зависимости от этого выбирать логику обработки.purchaseState
— статус платежа. Обратите внимание, что значения ключей тут отличаются от параметра paymentState в подписке. Возможные значения:0
— покупка оплачена.1
— покупка отменена, то есть она ожидала оплаты, но в итоге пользователь её не оплатил.2
— покупка ожидает оплаты. В некоторых странах покупку можно оплатить в магазине, то есть сначала пользователь делает покупку на устройстве, потом идёт в терминал и оплачивает там. В целом, это редкий кейс, но стоит иметь в виду.
acknowledgementState
— статус подтверждения покупки. Как и в транзакциях подписки, этот параметр нужен для подтверждения, получил ли пользователь доступ оплаченным функциям/коненту. Значение0
означает, что не получил,1
— получил. За простановку этого статуса отвечает разработчик, сделать это можно и на стороне мобильного приложения, и на стороне сервера. Если не подтвердить статус покупки, то через 3 дня автоматически произойдёт рефанд. Рекомендую реализовать логику, что если в транзакции пришёлacknowledgementState=0
, то сервер его изменяет. Ниже расскажу, как это делается.consumptionState
— статус потребления продукта. Это то, что в iOS называется consumable. Задаётся со стороны мобильного приложения Значение0
означает, что продукт не употреблён, а1
— употреблён. Если вы продаёте доступ к приложению навсегда или доступ к определённой функциональности приложения навсегда, то такой продукт не нужно употреблять, то есть статус должен быть0
. Если же вы продаёте монетки, и пользователь может покупать их много раз, то такие продукт нужно употреблять, то есть статус должен быть1
.consumptionState=0
означает, что продукт можно купить только один раз, аconsumptionState=1
— много раз.orderId
— уникальный идентификатор транзакции. У каждой покупки будет свой идентификатор, его можно использовать, чтобы понимать, была ли ранее обработана данная транзакция.purchaseTimeMillis
— дата покупки.regionCode
— страна аккаунта, с которого совершена покупка в двухсимвольном формате, например, RU. Обратите внимание, что название этого параметра отличается от подписки, там он называется countryCode.purchaseType
— тип покупки. В большинстве случаев этого ключа нет. Но при этом его важно учитывать, потому что с помощью него можно понимать, что покупка была совершена в Sandbox окружении. Возможные значения:0
— покупка из Sandbox окружения, то есть её не нужно учитывать в аналитике.1
— покупка оплачена промокодом.2
— покупка выдана за целевое действие, например, пользователь смотрел рекламу вместо оплаты.
Как видите, в целом валидация продукта похожа на валидацию подписки, но есть странные моменты:
Не возвращается цена, хотя это очень удобно для аналитики.
В параметре
purchaseState
значения сильно отличаются от значенийpaymentState
в подписке, если это не учитывать, то будут ошибки.Возвращается
regionCode
, хотя в подписке он называетсяcountryCode
.
Покупку продуктов надо подтверждать, так же как и покупку подписки. Для этого необходимо вызвать метод purchases.products.acknowledge, который сделает POST-запрос https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge
.
Если покупка ещё не оплачена, то её не надо подтверждать.
Отслеживание рефандов (возвратов денег) подписок и продуктов
Для качественной аналитики вам необходимо учитывать рефанды. К сожалению, информация о рефанде не приходит внутри транзакции или отдельным событием, как это работает в iOS. Для получения списка транзакций, по которым вернули деньги, надо с определённой периодичностью (например, раз в сутки) вызывать метод purchases.voidedpurchases.list, который выполняет GET-запрос https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/voidedpurchases
. В ответ вернётся список всех транзакций, по которым произошёл возврат денег. Рекомендую использовать для поиска транзакций в базе параметр orderId
, а не purchaseToken
. Во-первых, так будет быстрее, а во-вторых, у всех продлений подписки будет один и тот же токен, и вам надо находить последний из них.
Серверные уведомления о транзакциях
Серверные уведомления (Real-time developer notifications) позволяют получать информацию о событии, которое произошло на стороне Google, на вашем сервере практически моментально. После их настройки вы будете получать информацию о новых покупках, продлениях, проблемах с платежами и так далее. Это позволяет собирать более точную аналитику, а также упрощает менеджмент состояния подписчика.
Для того, чтобы получать серверные уведомления, необходимо в Google Cloud Pub/Sub создать топик, который будет отправлять уведомления по желаемому адресу. После этого данный топик необходимо указать в разделе Monetization setup в Google Play Console. Подробнее об этом со скриншотами вы можете прочитать в документации Adapty.
Серверное уведомление
{
"message": {
"data": "eyJ2ZXJzaW9uIjoiMS4wIiwicGFja2FnZU5hbWUiOiJjb20uYWRhcHR5LnNhbXBsZV9hcHAiLCJldmVudFRpbWVNaWxsaXMiOiIxNjMwNTI5Mzk3MTI1Iiwic3Vic2NyaXB0aW9uTm90aWZpY2F0aW9uIjp7InZlcnNpb24iOiIxLjAiLCJub3RpZmljYXRpb25UeXBlIjo2LCJwdXJjaGFzZVRva2VuIjoiY2o3anAuQU8tSjFPelIxMjMiLCJzdWJzY3JpcHRpb25JZCI6ImNvbS5hZGFwdHkuc2FtcGxlX2FwcC53ZWVrbHlfc3ViIn19",
"messageId": "2829603729517390",
"message_id": "2829603729517390",
"publishTime": "2021-09-01T20:49:59.124Z",
"publish_time": "2021-08-04T20:49:59.124Z"
},
"subscription": "projects/935083/subscriptions/adapty-rtdn"
}
Нас в первую очередь интересует ключ data
, в котором лежит информация о транзакции, закодированная с помощью base64. Ключ messageId
можно использовать для дедупликации сообщений, чтобы не обрабатывать повторно пришедшие сообщения.
Транзакция в серверном уведомлении
{
"version": "1.0",
"packageName": "com.adapty.sample_app",
"eventTimeMillis": "1630529397125",
"subscriptionNotification": {
"version": "1.0",
"notificationType": 6,
"purchaseToken": "cj7jp.AO-J1OzR123",
"subscriptionId": "com.adapty.sample_app.weekly_sub"
}
}
С помощью ключа packageName
можно понять, к какому приложению относится событие. Ключ subscriptionId
говорит, о какой подписке идёт речь, а с помощью purchaseToken
мы можем найти конкретную транзакцию. В случае с подписками надо всегда искать последнюю транзакцию в цепочке продления, событие относится именно к ней. В ключе notificationType
указан тип события. На мой взгляд, самые полезные для подписок:
2: SUBSCRIPTION_RENEWED
— подписка успешно продлилась.3: SUBSCRIPTION_CANCELED
— пользователь выключил автопродление подписки. Если автопродление выключили, надо пытаться вернуть пользователя в число активных подписчиков.5: SUBSCRIPTION_ON_HOLD
,6: SUBSCRIPTION_IN_GRACE_PERIOD
— подписку не получилось продлить из-за проблем с оплатой. Стоит сообщить пользователю об этом, чтобы у него не отменилась подписка автоматически.12: SUBSCRIPTION_REVOKED
— подписка отозвана. Нужно закрыть пользователю доступ к функциям, которые давала эта покупка.
Для продуктов (не подписок) вместо ключа subscriptionNotification
будет приходить oneTimeProductNotification
, и в нём вместо ключа subscriptionId
будет sku
. Также для продуктов приходит всего 2 типа события:
1: ONE_TIME_PRODUCT_PURCHASED
— успешная покупка продукта.2: ONE_TIME_PRODUCT_CANCELED
— покупка продукта отменилась, потому что пользователь не оплатил его.
Заключение
Серверная валидация сильно снижает возможность несанкционированного доступа к платному контенту вашего приложения и улучшает качество аналитики. При настройке серверной валидации придётся учитывать много сайд-кейсов: upgrade подписки, смена подписки, триал, introductory price, возвраты. Кроме того, есть ещё нюансы, вроде того, что Google снижает комиссию с 30% до 15% для подписок, которые продлеваются больше года. Так что весь процесс долгий и трудоёмкий.
Если хотите сэкономить ресурсы своей команды разработчиков, то советую присмотреться к Adapty.
Про Adapty
Adapty не только упрощает техническую реализацию подписок:
Встроенная аналитика позволяет быстро понять основные метрики приложения.
Когортный анализ отвечает на вопрос, как быстро сходится экономика.
А/Б тесты увеличивают выручку приложения.
Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики.
Промо кампании уменьшают отток аудитории.
Open source SDK позволяет интегрировать подписки в приложение за несколько часов.
Серверная валидация и API для работы с другими платформами.
Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии.