Внутриигровые покупки

Внимание. Функциональность доступна в SDKv2.
Вы можете получать доход, предоставив пользователям возможность совершать покупки в игре. Например, дополнительное время на прохождение уровня или аксессуары для игрового персонажа. Для этого:

Совершать внутриигровые покупки могут только авторизованные на Яндексе пользователи, так как это позволяет сохранить информацию о покупках игрока. Авторизоваться пользователь может непосредственно во время игры, в том числе и в момент совершения покупки. Поэтому важно предусмотреть и заранее разрешить возможные конфликты, которые могут возникнуть при объединении локального игрового прогресса с данными на сервере.

  1. Инициализация
  2. Активация процесса покупки
  3. Получение списка купленных товаров
  4. Получение каталога всех товаров
  5. Обработка покупки и начисление внутриигровой валюты
  6. Проверка необработанных покупок
  7. Защита от накруток

Инициализация

Чтобы предоставить пользователям возможность совершать внутриигровые покупки, используйте объект payments. Так как покупки доступны только авторизованным игрокам, сразу после инициализации SDK вызовите метод ysdk.getPlayer().
var payments = null;
ysdk.getPayments({ signed: true }).then(_payments => {
        // Покупки доступны.
        payments = _payments;
    }).catch(err => {
        // Покупки недоступны. Включите монетизацию в консоли разработчика.
        // Убедитесь, что на вкладке Покупки консоли разработчика присутствует таблица
        // хотя бы с одним внутриигровым товаром и надписью «Покупки разрешены».
    })

signed: true — опциональный параметр. Возвращает параметр signature в методах payments.getPurchases() и payments.purchase(). Предназначен для защиты игры от накруток.

Активация процесса покупки

Активировать внутриигровую покупку можно следующим методом:
payments.purchase(id, developerPayload)
  • id — string — идентификатор товара, который был задан в консоли разработчика.
  • developerPayload — string — опциональный параметр. Дополнительная информация о покупке, которая вы хотите передавать на свой сервер (будет передана в параметре signature).

Метод открывает фрейм с платежным шлюзом. Возвращает Promise<Purchase>.

Purchase — object — информация о покупке. Содержит свойства:
  • productID — string — идентификатор продукта.
  • purchaseToken — string — токен для использования покупки.
  • developerPayload — string — дополнительные данные о покупке.
  • signature — string — данные о покупке и подпись для проверки подлинности игрока. Подробнее.

После того как игрок успешно совершил покупку, Promise переходит в состояние «resolved». Если игрок не совершил покупку и закрыл окно, Promise переходит в состояние «rejected».

Если игрок в момент покупки не авторизован на Яндексе, откроется окно авторизации. Также вы можете предложить пользователю авторизоваться заранее.

Примечание. Многие пользователи Яндекс.Игр не авторизованы, поэтому важно поддерживать возможность игры для неавторизованного игрока. При этом вы можете периодически предлагать пользователю авторизоваться, информируя о преимуществах, которые он получит.
Пример

В общем случае:

payments.purchase('gold500').then(purchase => {
        // Покупка успешно совершена!

    }).catch(err => {
        // Покупка не удалась: в консоли разработчика не добавлен товар с таким id,
        // пользователь не авторизовался, передумал и закрыл окно оплаты,
        // истекло отведенное на покупку время, не хватило денег и т. д.
    })

С использованием опционального параметра developerPayload:

payments.purchase({ id: 'gold500', developerPayload: '{serverId:42}' }).then(purchase => {
        // purchase.developerPayload === '{serverId:42}'
    })

Получение списка купленных товаров

Чтобы узнать, какие покупки игрок уже совершил, используйте метод:
payments.getPurchases()

Метод возвращает Promise<Purchase[]>.

Purchase[] — object — список покупок, совершенных игроком. Содержит свойства:
  • productID — string — идентификатор продукта.
  • purchaseToken — string — токен для использования покупки.
  • developerPayload — string — дополнительные данные о покупке.
  • signature — string — данные о покупке и подпись для проверки подлинности игрока. Подробнее.
Пример
var SHOW_ADS = true;
payments.getPurchases().then(purchases => {
        if (purchases.some(purchase => purchase.productID === 'disable_ads')) {
          SHOW_ADS = false;
        }
    }).catch(err => {
        // Выбрасывает исключение USER_NOT_AUTHORIZED для неавторизованных пользователей.
    })

Получение каталога всех товаров

Чтобы получить список доступных покупок, используйте метод:
payments.getCatalog()

Метод возвращает Promise<Product[]>.

Product[] — object — список доступных для пользователя товаров. Формируется из таблицы на вкладке Покупки консоли разработчика. Содержит свойства:
  • productID — string — идентификатор продукта.
  • title — string — название.
  • description — string — описание.
  • imageURI — string — URL изображения.
  • price — string — стоимость товара в формате <цена><значок валюты>. Например, «25P».
  • priceCurrencyCode — string — код валюты.
Пример
var gameShop = []
payments.getCatalog().then(products => {
        gameShop = products;
    });

Обработка покупки и начисление внутриигровой валюты

Существуют два типа покупок — постоянные (например, отключение рекламы) и используемые (например, внутриигровая валюта).

Для обработки постоянных покупок применяйте метод payments.getPurchases().

Для обработки используемых покупок применяйте метод payments.consumePurchase().

payments.consumePurchase()

Метод возвращает Promise в состоянии «resolved», если обработка прошла успешна, или «rejected» если возникли ошибки.

Внимание. После вызова метода payments.consumePurchase() обработанная покупка удаляется без возможности восстановления. Поэтому сначала модифицируйте данные игрока методами player.setStats() или player.incrementStats(), а затем обрабатывайте покупку.
Пример
payments.purchase('gold500').then(purchase => {
        // Покупка успешно совершена!
        // Начисляем на баланс 500 золотых и используем покупку.
        addGold(500).then(() => payments.consumePurchase(purchase.purchaseToken));
    });

function addGold(value) {
        return player.incrementStats({ gold: value });
        // Подробнее см. в разделе Данные игрока.
}

purchaseToken — string — токен, возвращаемый методами payments.purchase() и payments.getPurchases().

Проверка необработанных покупок

Если во время совершения внутриигровой покупки у пользователя отключился интернет, или ваш сервер был недоступен, покупка может остаться необработанной. Чтобы избежать такой ситуации, проверяйте наличие необработанных покупок с помощью метода payments.getPurchases(), например, при каждом запуске игры.

Пример для игры, данные которой хранятся на сервере Яндекса
payments.getPurchases().then(purchases => purchases.forEach(consumePurchase));

function consumePurchase(purchase) {
    if (purchase.productID === 'gold500') {
        player.incrementStats({ gold: 500 }).then(() => {
                payments.consumePurchase(purchase.purchaseToken)
            });
    }
}
Пример для игры, данные которой хранятся на сервере разработчика
payments.getPurchases().then(purchases => {
        fetch('https://your.game.server?purchase', {
            method: 'POST',
            headers: { 'Content-Type': 'text/plain' },
            body: purchases.signature
        });
    });

Защита от накруток

Вы можете обезопасить себя от возможных накруток показателей в игре. Для этого вместо методов player.setStats() и player.incrementStats() используйте для обработки покупок функцию serverPurchase(signature).

Внимание. Для работы функции serverPurchase(signature) требуется настроить сохранение игровых данных на сервере разработчика и инициализировать покупки с параметром signed: true.
// Убедитесь что покупки инициализированы с параметром { signed: true }

payments.purchase('gold500').then(purchase => {
        // Покупка успешно совершена!
        // Начисляем на сервере 500 золотых...
        serverPurchase(purchase.signature.slice(1)); // fail — проверьте подпись.
        serverPurchase(purchase.signature); // ok — покупка подтверждена.
        serverPurchase(purchase.signature); // fail — проверьте уникальность покупки.
    });

function serverPurchase(signature) {
    return fetch('https://your.game.server?purchase', {
            method: 'POST',
            headers: { 'Content-Type': 'text/plain' },
            body: signature
        });
}

Параметр signature передаваемого на сервер запроса содержит данные о покупке и подпись. Представляет собой две строки в кодировке base64: <подпись>.<JSON с данными о покупке>.

Пример signature
hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=
Пример передаваемых данных о покупке (в формате JSON)

Обратите внимание, что формат данных параметра signature в функции serverPurchase(signature) отличается от используемого в методе payments.getPurchases().

В методе payments.getPurchases() параметр signature содержит массив объектов покупок в поле data. В функции serverPurchase(signature) — объект покупки.

{
  "algorithm": "HMAC-SHA256",
  "issuedAt": 1571233371,
  "requestPayload": "qwe",
  "data": {
    "token": "d85ae0b1-9166-4fbb-bb38-6d2a4ca4416d",
    "status": "waiting",
    "errorCode": "",
    "errorDescription": "",
    "url": "https://yandex.ru/games/sdk/payments/trust-fake.html",
    "product": {
      "id": "noads",
      "title": "Без рекламы",
      "description": "Отключить рекламу в игре",
      "price": {
        "code": "RUR",
        "value": "49"
      },
      "imagePrefix": "https://avatars.mds.yandex.net/get-games/1892995/2a0000016d1c1717bd7a0149ccadc86078a1/"
    },
    "developerPayload": "TEST DEVELOPER PAYLOAD"
  }
}
Пример секретного ключа
t0p$ecret

Секретный ключ для проверки подписи является уникальным для игры. Формируется автоматически при создании покупок в консоли разработчика. Размещен под таблицей с покупками.

Пример проверки подписи на сервере
import hashlib
import hmac
import base64
import json

usedTokens = {}

key = 't0p$ecret' # Держите ключ в секрете.
secret = bytes(key, 'utf-8')
signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0='

sign, data = signature.split('.')
message = base64.b64decode(data)

purchaseData = json.loads(message)
result = base64.b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
if result.decode('utf-8') == sign:
  print('Signature check ok!')

  if not purchaseData['data']['token'] in usedTokens:
    usedTokens[purchaseData['data']['token']] = True; # Используйте базу данных.
    print('Double spend check ok!')

    print('Apply purchase:', purchaseData['data']['product'])
    # Здесь можно безопасно начислить купленное.

Если при использовании SDK Яндекс.Игр вы столкнулись с проблемой или у вас появился вопрос, напишите в службу поддержки.