Как стать автором
Обновить
0
Adapty
Сервис для аналитики и роста мобильных подписок

Android in-app purchases, часть 5: серверная валидация покупок

Время на прочтение14 мин
Количество просмотров10K

Всем привет, я Кирилл, СТО Adapty. Я делал систему серверной валидации для наших SDK и сегодня расскажу про то, как её настроить для приложений на Android. 

Это пятая статья из серии о внедрении внутренних покупок на Android, советую познакомиться с остальными:

  1. Android in-app purchases, часть 1: конфигурация и добавление в проект.

  2. Android in-app purchases, часть 2: инициализация и обработка покупок.

  3. Android in-app purchases, часть 3: получение активных покупок и смена подписки.

  4. Android in-app purchases, часть 4: коды ошибок от Billing Library и как не облажаться с тестированием.

  5. Android in-app purchases, часть 5: серверная валидация покупок. — Вы тут.

Что такое серверная валидация покупки?

Серверная валидация позволяет проверить подлинность покупки: устройство обращается к серверам Google за информацией, действительно ли была совершена покупка, и валидна ли она.

Зачем валидировать покупки

Серверная валидация необязательна, покупки будут работать без неё. Но с ней лучше, потому что она даёт важные преимущества:

  1. Продвинутая аналитика платежей. Это особенно актуально для подписок. После того, как подписка активирована, списания происходят без участия устройства. Если пользователь не заходит в приложение, без серверной обработки мы не сможем своевременно получать информацию о статусе подписки: продлил, отменил, возникла проблема с платежом. 

  2. Проверка подлинности покупки. Серверная валидация позволяет убедиться, что транзакция действительно произошла, и покупка была не фродовой. Поскольку на Android процент мошеннических операций выше, чем на iOS, то в приложениях на Google Play об этом особенно важно позаботиться.

  3. Кроссплатформенные подписки, имея данные по статусу подписки в живом режиме, можно синхронизировать подписки пользователя с другими платформами. Так, если пользователь подписался на приложение на Android, доступ к нему у него будет на iOS, Web и других платформах. 

  4. Возможность контролировать доступ к контенту со стороны сервера. Серверная валидация не даст доступ к контенту тем пользователям, которые хотят получить его, отправляя запросы к серверу.

Вообще уже один первый пункт стоит того, чтобы настроить серверную валидацию.

Валидация платежей

В целом процесс валидации платежей на Android можно описать схемой:

Серверная валидация покупок в приложениях на Android
Серверная валидация покупок в приложениях на 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": ""
}

Чтобы понимать, есть ли у пользователя доступ к платным функциям приложения, то есть имеется ли у него активная подписка, необходимо:

  1. Проверить параметры startTimeMillis и expiryTimeMillis, текущее время должно быть между ними.

  2. Кроме того, необходимо убедиться, что параметр paymentState не равен 1. Это означает, что подписка ожидает оплаты и в настоящий момент не нужно давать доступ к платным функциям приложения.

  3. Если в транзакции присутствует поле 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 для работы с другими платформами.

Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии.

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии0

Публикации

Информация

Сайт
adapty.io
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
США

Истории