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

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

Портальная валюта

Ян (Yan) — это портальная валюта платформы Яндекс Игры для оплаты внутриигровых покупок. Яны хранятся на едином для всех игр балансе игрока, который можно пополнить с помощью банковских карт. Курс яна к рублю динамический.

Примечание

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

Пополнить баланс можно:

  • в шапке каталога;
  • в профиле игрока;
  • во время покупки в игре.

Пользователи также могут получать яны бонусом за участие в акциях или покупку фиксированных паков.

Совершать внутриигровые покупки могут как авторизованные на Яндексе пользователи, так и неавторизованные. Авторизоваться пользователь может непосредственно во время игры, в том числе и в момент совершения покупки.

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

Условия подключения

После добавления покупок и публикации черновика игры отправьте письмо с запросом о подключении покупок на почту games-partners@yandex-team.ru. В письме обязательно укажите название и идентификатор (ID) игры.

После получения ответного письма от games-partners@yandex-team.ru с подтверждением, что покупки разрешены, их можно будет настраивать и тестировать.

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

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

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 — информация о покупке. Содержит свойства:

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

Внимание

Нестабильная работа интернета может привести к ситуации, когда игрок совершил покупку, но она не учлась в игре. Чтобы избежать этого, для обработки покупок применяйте методы, описанные в разделах Проверка необработанных покупок и Обработка покупки и начисление внутриигровой валюты.

Отказ от следования этим инструкциям может привести к отключению внутриигровых покупок в приложении или снятию его с публикации.

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

Пример

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

payments.purchase({ id: '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[] — array — список покупок, совершенных игроком. У списка есть свойство:

Каждая покупка Purchase содержит свойства:

  • productID — string — идентификатор товара.
  • purchaseToken — string — токен для использования покупки.
  • developerPayload — 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 — список доступных для пользователя товаров. Формируется из таблицы на вкладке Покупки консоли разработчика. Каждый Product содержит свойства:

  • id — string — идентификатор товара.
  • title — string — название.
  • description — string — описание.
  • imageURI — string — URL изображения.
  • price — string — стоимость товара в формате <цена> <код валюты>. Например, «25 YAN».
  • priceValue — string — стоимость товара в формате <цена>. Например, «25».
  • priceCurrencyCode — string — код валюты («YAN»).

Также у Product есть метод получения адреса иконки валюты:

getPriceCurrencyImage(size: ECurrencyImageSize = ECurrencyImageSize.SMALL) — string — адрес иконки, например //yastatic.net/s3/games-static/static-data/images/payments/sdk/currency-icon-s@2x.png. Значением параметра size можно указать medium, small или svg.

Примечание

Чтобы указать цену продукта, используйте один из вариантов:

  • price;
  • priceValue и иконка из getPriceCurrencyImage.

Пример

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({ id: '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({ id: '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": "YAN",
        "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'])
    # Здесь можно безопасно начислить купленное.

const crypto = require('crypto');

const usedTokens = {};

const key = 't0p$ecret'; // Держите ключ в секрете.
const signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=';

const [sign, data] = signature.split('.');
const purchaseDataString = Buffer.from(data, 'base64').toString('utf8');
const hmac = crypto.createHmac('sha256', key);

hmac.update(purchaseDataString);

const purchaseData = JSON.parse(purchaseDataString);

if (sign === hmac.digest('base64')) {
  console.log('Signature check ok!');

  if (!usedTokens[purchaseData.data.token]) {
    usedTokens[purchaseData.data.token] = true; // Используйте базу данных.
    console.log('Double spend check ok!');

    console.log('Apply purchase:', purchaseData.data.product);
    // Здесь можно безопасно начислить купленное.
  }
}


Примечание

Сотрудники службы поддержки помогают разместить готовую игру или WebApp на платформе Яндекс Игр. На прикладные вопросы о разработке и тестировании предметно ответят другие разработчики в Сообществе в Telegram.

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

Написать в чат Написать письмо