应用内购买
您可以通过为用户提供游戏内购买的选项来赚取收入。例如,额外的关卡通关时间或游戏角色配饰。为此:
连接条件
在使用SDK之前,请检查合作方案。为此,在开发人员控制台中转到Account部分,并检查Unified licensing model字段的值:
启用变现和购买:
-
启用广告变现。在Yandex Advertising Network合作伙伴界面中指定广告和购买付款的详细信息。数据验证后,Yandex Advertising Network界面中Extras → Documents部分的合同状态将更改为Offer accepted。
-
向games-partners@yandex-team.com发送启用请求的电子邮件。在邮件中注明:
- 游戏名称
- 游戏标识符(ID)
提示
请尽早发送请求,您可以在上传游戏存档或添加购买项之前执行此操作。
-
等待来自games-partners@yandex-team.com的回复邮件,确认购买已被允许。
有关说明的后续步骤,请参阅连接购买功能部分。
您所有游戏中的购买功能已自动启用。继续进行初始化。
初始化
要让玩家能够进行应用内购买,请使用 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 参数中传递)。 |
成功完成购买后,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[]> {}
该方法返回用户可用的产品列表。根据开发人员控制台 In-app purchases 选项卡中的表格生成。每个 IProduct 包含以下属性:
|
属性 |
类型 |
说明 |
|
|
|
商品标识符。 |
|
|
|
商品名称。 |
|
|
|
商品说明。 |
|
|
|
商品图片的URL。 |
|
|
|
商品价格,格式为 |
|
|
|
商品价格,格式为 |
|
|
|
货币代码。 |
|
|
|
根据图标大小参数获取货币图标地址的方法。可能的值:
|
示例
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> {}
接受 payments.purchase() 和 payments.getPurchases() 方法返回的 purchaseToken。如果处理成功,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}
防作弊保护
为了保护自己免受游戏中可能的刷分操作,应在服务器端处理购买:
- 使用
{ signed: true }参数初始化YaGames.init()或ysdk.getPayments()。 - 将 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格式)
重要
serverPurchase(signature) 函数中 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
用于验证签名的密钥对于游戏是唯一的。在开发人员控制台中创建购买时自动生成。密钥显示在 In-app purchases → Settings 选项卡中。
服务器上的签名验证示例
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}
备注
技术支持团队将协助您将已完成的游戏发布到 Yandex 游戏平台。关于开发和测试方面的具体问题,其他开发人员将在Discord 频道中进行回答。
如果您遇到 Yandex Games SDK 方面的问题或有其他问题想要咨询,请联系支持部门:
传递到服务器的请求的 signature 参数包含有关购买的数据和签名。它由两个 base64 编码的字符串组成:<签名>.<有关购买的 JSON 数据>。