Инап-покупки
Вы можете получать доход, предоставив пользователям возможность совершать покупки в игре. Например, дополнительное время на прохождение уровня или аксессуары для игрового персонажа. Для этого:
- Подключите инап-покупки в Консоли разработчика Яндекс Игр.
- Настройте в SDK возможность работы с покупками.
- Добавьте проверку необработанных покупок.
- Протестируйте покупки.
Внимание
Тестировать покупки можно только после подключения их консумирования. Иначе могут появиться необработанные платежи, которые сделают прохождение модерации невозможным.
Условия подключения
Перед работой с SDK проверьте схему сотрудничества. Для этого в Консоли разработчика перейдите в раздел Профиль и проверьте значение поля Единая лицензионная схема:
Подключите монетизацию и покупки:
-
Подключите рекламную монетизацию. В партнерском интерфейсе РСЯ укажите реквизиты для выплат по рекламе и покупкам. После проверки данных статус договора в интерфейсе РСЯ в разделе Дополнительно → Документы изменится на Оферта акцептована.
-
Отправьте письмо с запросом о подключении на почту games-partners@yandex-team.ru. В письме укажите:
- название игры;
- идентификатор (ID) игры.
Совет
Отправьте запрос как можно раньше, вы можете сделать это до загрузки архива игры или добавления покупок.
-
Дождитесь ответного письма от games-partners@yandex-team.ru с подтверждением, что покупки разрешены.
Дальнейшие шаги инструкции см. в разделе Подключить покупки.
Покупки во всех ваших играх включены автоматически. Перейдите к инициализации.
Инициализация
Чтобы игроки могли совершать инап-покупки, используйте объект payments. Вы можете:
-
Обратиться напрямую к
ysdk.payments. Покупки инициализируются при первом вызове любого из методов объекта, из-за чего первый вызов может быть чуть медленнее. -
Инициализировать объект с помощью метода
ysdk.getPayments(). Он предзагружает данные, необходимые для методовpayments. Так их первый вызов не будет замедлен.
Внимание
В YaGames.init() и ysdk.getPayments() можно передать опциональный параметр signed: boolean, который предназначен для защиты от накруток. Выбор значения зависит от того, где обрабатываются платежи:
-
Если на стороне клиента — вызовите методы без параметра или передайте
signed: false. Методы покупок будут возвращать данные в открытом виде. -
Если на стороне сервера — передайте
signed: true. В таком случае в ответах методов payments.getPurchases() и payments.purchase() все данные возвращаются только в зашифрованном виде в параметре signature.
Инициализация с параметром по умолчанию (signed: false).
Способ 1: Упрощенный
1const ysdk = await YaGames.init();
2
3const payments = ysdk.payments;
Способ 2: Предзагрузка через getPayments()
1const ysdk = await YaGames.init();
2
3try {
4 const payments = await ysdk.getPayments();
5} catch (err) {
6 // Покупки недоступны.
7}
Инициализация с параметром signed: true.
Способ 1: При инициализации SDK
1const ysdk = await YaGames.init({ signed: true });
2
3const payments = ysdk.payments;
Способ 2: Более тонкая настройка через getPayments()
1const ysdk = await YaGames.init();
2
3try {
4 const payments = await ysdk.getPayments({ signed: true });
5} catch (err) {
6 // Покупки недоступны.
7}
Активация процесса покупки
Чтобы активировать инап-покупку, используйте метод payments.purchase(). Он открывает фрейм с платежным шлюзом.
Сигнатура метода:
1function purchase(data: {
2 id: string;
3 developerPayload?: string;
4}) => Promise<IPurchase | ISign> {}
Принимает параметры:
|
Параметр |
Тип |
Описание |
|
|
|
Идентификатор товара, который задан в Консоли разработчика. |
|
|
|
Опциональный параметр. Содержит дополнительную информацию о покупке, которую вы хотите передавать на свой сервер (будет передана в параметре signature). |
Инициализация с параметром по умолчанию (signed: false).
Возвращает Promise<IPurchase> с информацией о покупке.
1interface IPurchase {
2 productID: string;
3 purchaseToken: string;
4 developerPayload: string;
5}
Содержит:
|
Параметр |
Тип |
Описание |
|
|
|
Идентификатор товара. |
|
|
|
Токен для использования покупки. |
|
|
|
Дополнительная информация о покупке. |
Инициализация с параметром signed: true.
Возвращает Promise<ISign>.
1interface ISign {
2 signature: string;
3}
Содержит:
|
Параметр |
Тип |
Описание |
|
|
|
Зашифрованные данные о покупке и подпись для проверки подлинности игрока. |
После успешного совершения покупки Promise разрешается со статусом fulfilled. Если игрок не совершил покупку и закрыл окно, Promise отклоняется со статусом rejected.
Внимание
Нестабильная работа интернета может привести к ситуации, когда игрок совершил покупку, но она не была обработана в игре. Чтобы избежать этого, для обработки покупок применяйте методы, описанные в разделах Проверка необработанных покупок и payments.consumePurchase().
Отказ от следования этим инструкциям может привести к отключению инап-покупок в приложении или снятию его с публикации.
Пользователь может совершить покупку без авторизации, но мы рекомендуем предлагать ему войти в аккаунт заранее или при совершении покупки.
Пример
В общем случае:
1const ysdk = await YaGames.init();
2
3try {
4 const purchase = await ysdk.payments.purchase({ id: 'gold500' });
5} catch (err) {
6 // Покупка не удалась: в Консоли разработчика не добавлен товар с таким id,
7 // пользователь не авторизовался, передумал и закрыл окно оплаты,
8 // истекло отведенное на покупку время, не хватило денег и т. д.
9}
С использованием опционального параметра developerPayload:
1const ysdk = await YaGames.init();
2
3try {
4 const purchase = await ysdk.payments.purchase({ id: 'gold500', developerPayload: '{serverId:42}' });
5} catch (err) {
6 // Обработка ошибки покупки.
7}
Получение списка купленных товаров
Используйте метод payments.getPurchases(), чтобы:
-
узнать, какие покупки игрок уже совершил;
-
проверить наличие необработанных покупок;
-
обработать постоянные покупки.
Сигнатура метода:
function getPurchases(): Promise<IPurchase[] | ISign> {}
Инициализация с параметром по умолчанию (signed: false).
Возвращает Promise<IPurchase[]> с массивом покупок. Каждый элемент массива имеет тот же формат, что и покупка, возвращаемая методом payments.purchase().
Пример
1const ysdk = await YaGames.init();
2
3let SHOW_ADS = true;
4
5try {
6 const purchases = await ysdk.payments.getPurchases();
7
8 if (purchases.some(purchase => purchase.productID === 'disable_ads')) {
9 SHOW_ADS = false;
10 }
11} catch (err) {
12 // Ошибка получения списка покупок. Выбрасывает исключение PAYMENT_FAILURE.
13}
Инициализация с параметром signed: true.
Возвращает Promise<ISign>.
Содержит:
|
Параметр |
Тип |
Описание |
|
|
|
Зашифрованные данные о покупке и подпись для проверки подлинности игрока. |
Пример
1const ysdk = await YaGames.init({ signed: true });
2
3try {
4 const purchases = await ysdk.payments.getPurchases();
5 // Отправляем список покупок на сервер.
6 const response = await fetch('https://your.game.server/handlePurchases', {
7 method: 'POST',
8 headers: { 'Content-Type': 'text/plain' },
9 body: purchases.signature
10 });
11} catch (err) {
12 // Ошибка получения списка покупок или их обработки.
13}
Получение каталога всех товаров
Чтобы получить список доступных покупок и их стоимость, используйте метод payments.getCatalog().
Сигнатура метода:
1interface IProduct {
2 id: string;
3 title: string;
4 description: string;
5 imageURI: string;
6 price: string;
7 priceValue: string;
8 priceCurrencyCode: string;
9 getPriceCurrencyImage(size: 'small' | 'medium' | 'svg'): string;
10}
11
12function getCatalog(): Promise<IProduct[]> {}
Метод возвращает список доступных для пользователя товаров. Формируется из таблицы на вкладке Инап-покупки Консоли разработчика. Каждый IProduct содержит свойства:
|
Свойство |
Тип |
Описание |
|
|
|
Идентификатор товара. |
|
|
|
Название товара. |
|
|
|
Описание товара. |
|
|
|
URL изображения товара. |
|
|
|
Стоимость товара в формате |
|
|
|
Стоимость товара в формате |
|
|
|
Код валюты. |
|
|
|
Метод получения адреса иконки валюты в зависимости от параметра размера иконки. Возможные значения:
|
Важно
Портальная валюта должна определяться автоматически (пункт 3.8). Для этого берите ее название и иконку из свойств IProduct. Подробнее см. в разделе Автоматическое определение портальной валюты.
Пример
1const ysdk = await YaGames.init();
2
3let gameShop = [];
4
5try {
6 const purchases = await ysdk.payments.getPurchases();
7
8 gameShop = purchases;
9} catch (err) {
10 // Ошибка получения списка покупок.
11}
Обработка покупки и начисление внутриигровой валюты
Существуют два типа покупок:
- Постоянные (например, отключение рекламы). Для их обработки применяйте метод payments.getPurchases().
- Используемые (например, внутриигровая валюта). Для их обработки применяйте метод
payments.consumePurchase().
payments.consumePurchase()
Внимание
После вызова метода payments.consumePurchase() обработанная покупка удаляется без возможности восстановления. Поэтому сначала модифицируйте данные игрока методами player.setData(), player.setStats() или player.incrementStats(), а затем обрабатывайте покупку.
Сигнатура метода:
function consumePurchase(purchaseToken: string): Promise<void> {}
Принимает purchaseToken, возвращаемый методами payments.purchase() и payments.getPurchases(). Если обработка прошла успешно, Promise разрешается со статусом fulfilled, если возникла ошибка — отклоняется со статусом rejected.
Пример
1const ysdk = await YaGames.init();
2
3function addGold(value) {
4 return ysdk.player.incrementStats({ gold: value });
5}
6
7try {
8 const purchase = await ysdk.payments.purchase({ id: 'gold500' });
9
10 await addGold(500);
11
12 await ysdk.payments.consumePurchase(purchase.purchaseToken);
13} catch (err) {
14 // Обработка ошибки обработки используемой покупки.
15}
Проверка необработанных покупок
Внимание
Эта проверка обязательна для прохождения модерации (пункт 1.13.1), поэтому важно настроить ее даже для тестовых покупок. Если добавить в игру покупки и протестировать их до настройки консумирования, то после тестов могут остаться необработанные платежи, которые сделают прохождение модерации невозможным.
Если во время совершения инап-покупки у пользователя отключился интернет или ваш сервер был недоступен, покупка может остаться необработанной. Чтобы этого избежать, проверяйте наличие необработанных покупок с помощью метода payments.getPurchases(), например, при каждом запуске игры.
Инициализация с параметром по умолчанию (signed: false).
Пример
1const ysdk = await YaGames.init();
2
3async function handlePurchase(purchase) {
4 if (purchase.productID === 'gold500') {
5 await ysdk.player.incrementStats({ gold: 500 });
6
7 await ysdk.payments.consumePurchase(purchase.purchaseToken);
8 }
9}
10
11const purchases = await ysdk.payments.getPurchases().then(purchases => purchases.forEach(consumePurchase));
12
13for (let purchase of purchases) {
14 await handlePurchase(purchase);
15}
Инициализация с параметром signed: true.
Пример
1const ysdk = await YaGames.init({ signed: true });
2
3try {
4 const purchases = await ysdk.payments.getPurchases();
5 // Отправляем список покупок на сервер.
6 const response = await fetch('https://your.game.server/handlePurchases', {
7 method: 'POST',
8 headers: { 'Content-Type': 'text/plain' },
9 body: purchases.signature
10 });
11} catch (err) {
12 // Ошибка получения списка покупок или их обработки.
13}
Защита от накруток
Чтобы обезопасить себя от возможных накруток показателей в игре, обрабатывайте покупки на стороне сервера:
- Инициализируйте
YaGames.init()илиysdk.getPayments()с параметром{ signed: true }. - Полученную подпись в ответах payments.purchase() и payments.getPurchases() передайте на свой сервер, расшифруйте ее с помощью секретного ключа.
- На своем сервере начислите игроку полученные в игре предметы.
1function serverPurchase(signature) {
2 return fetch('https://your.game.server/handlePurchase', {
3 method: 'POST',
4 headers: { 'Content-Type': 'text/plain' },
5 body: signature
6 });
7}
8
9// Убедитесь что покупки инициализированы с параметром { signed: true }.
10const ysdk = await YaGames.init({ signed: true });
11
12try {
13 const purchase = await ysdk.payments.purchase({ id: 'gold500' });
14
15 // Начисляем на сервере 500 золотых...
16 await serverPurchase(purchase.signature);
17} catch (err) {
18 // Ошибка покупки.
19}
Параметр signature передаваемого на сервер запроса содержит данные о покупке и подпись. Представляет собой две строки в кодировке base64: <подпись>.<JSON с данными о покупке>.
Пример signature
hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=
Пример передаваемых данных о покупке (в формате JSON)
Важно
Формат данных параметра signature в функции serverPurchase(signature) отличается от используемого в методе payments.getPurchases().
В методе payments.getPurchases() параметр signature содержит массив объектов покупок в поле data. В функции serverPurchase(signature) — объект покупки.
1{
2 "algorithm": "HMAC-SHA256",
3 "issuedAt": 1571233371,
4 "requestPayload": "qwe",
5 "data": {
6 "token": "d85ae0b1-9166-4fbb-bb38-6d2a4ca4416d",
7 "status": "waiting",
8 "errorCode": "",
9 "errorDescription": "",
10 "url": "https://yandex.ru/games/sdk/payments/trust-fake.html",
11 "product": {
12 "id": "noads",
13 "title": "Без рекламы",
14 "description": "Отключить рекламу в игре",
15 "price": {
16 "code": "YAN",
17 "value": "49"
18 },
19 "imagePrefix": "https://avatars.mds.yandex.net/get-games/1892995/2a0000016d1c1717bd7a0149ccadc86078a1/"
20 },
21 "developerPayload": "TEST DEVELOPER PAYLOAD"
22 }
23}
Пример секретного ключа
t0p$ecret
Секретный ключ для проверки подписи является уникальным для игры. Формируется автоматически при создании покупок в Консоли разработчика. Ключ отображается на вкладке Инап-покупки → Настройки.
Пример проверки подписи на сервере
1import hashlib
2import hmac
3import base64
4import json
5
6usedTokens = {}
7
8key = 't0p$ecret' # Держите ключ в секрете.
9secret = bytes(key, 'utf-8')
10signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0='
11
12sign, data = signature.split('.')
13message = base64.b64decode(data)
14
15purchaseData = json.loads(message)
16result = base64.b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
17if result.decode('utf-8') == sign:
18 print('Signature check ok!')
19
20 if not purchaseData['data']['token'] in usedTokens:
21 usedTokens[purchaseData['data']['token']] = True # Используйте базу данных.
22 print('Double spend check ok!')
23
24 print('Apply purchase:', purchaseData['data']['product'])
25 # Здесь можно безопасно начислить купленное.
1const crypto = require('crypto');
2
3const usedTokens = {};
4
5const key = 't0p$ecret'; // Держите ключ в секрете.
6const signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=';
7
8const [sign, data] = signature.split('.');
9const purchaseDataString = Buffer.from(data, 'base64').toString('utf8');
10const hmac = crypto.createHmac('sha256', key);
11
12hmac.update(purchaseDataString);
13
14const purchaseData = JSON.parse(purchaseDataString);
15
16if (sign === hmac.digest('base64')) {
17 console.log('Signature check ok!');
18
19 if (!usedTokens[purchaseData.data.token]) {
20 usedTokens[purchaseData.data.token] = true; // Используйте базу данных.
21 console.log('Double spend check ok!');
22
23 console.log('Apply purchase:', purchaseData.data.product);
24 // Здесь можно безопасно начислить купленное.
25 }
26}
Примечание
Сотрудники службы поддержки помогают разместить готовую игру на платформе Яндекс Игр. На прикладные вопросы о разработке и тестировании предметно ответят другие разработчики в Сообществе в Телеграме.
Если при использовании SDK Яндекс Игр вы столкнулись с проблемой или у вас появился вопрос, обратитесь в службу поддержки:
Параметр signature передаваемого на сервер запроса содержит данные о покупке и подпись. Представляет собой две строки в кодировке base64: <подпись>.<JSON с данными о покупке>.
- Включите монетизацию.
- В Консоли разработчика перейдите на вкладку Инап-покупки и убедитесь, что:
- Присутствует таблица хотя бы с одним внутриигровым товаром.
- Отображается надпись Покупки подключены.