游戏内购

您可以通过为用户提供游戏内购买的选项来赚取收入。例如,他们可通过购买额外的时间来完成关卡或获得人物装饰。为此,您需要:

注意

只有在连接它们的消费之后,才能测试购买。否则,可能会出现未处理的支付,这将使通过审核变得不可能

条件

在添加购买项并发布游戏草案后,发送邮件至 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:简化版

YaGames.init() // 或 YaGames.init({ signed: false })
    .then(ysdk => {
        // 购买方法可通过 ysdk.payments 访问
        console.log(ysdk.payments);
    }).catch(err => {
        // 无法使用购买功能
    });

方式2:通过 getPayments() 预加载

var payments = null;
YaGames.init()
    .then(ysdk => {
        ysdk.getPayments()
            .then(_payments => {
                // 购买方法可通过 payments 访问
                payments = _payments;
                console.log(payments);
            }).catch(err => {
                // 无法使用购买功能
            });
    });

使用 signed: true 参数初始化。

方式1:初始化 SDK 时设置

YaGames.init({ signed: true }) // 启用购买的签名功能
    .then(ysdk => {
        // 购买方法可通过 ysdk.payments 访问
        console.log(ysdk.payments);
    }).catch(err => {
        // 无法使用购买功能
    });

方式2:通过 getPayments() 精细配置

var payments = null;
YaGames.init()
    .then(ysdk => {
        ysdk.getPayments({ signed: true })
            .then(_payments => {
                // 购买方法可通过 payments 访问
                payments = _payments;
                console.log(payments);
            }).catch(err => {
                // 无法使用购买功能
            });
    });

 

激活购买流程

要激活游戏内购买,请使用方法 payments.purchase({ id, developerPayload })

  • id: string — 开发者仪表板中设置的产品 ID。
  • developerPayload: string — 可选参数。您想发送到服务器的其他购买数据(在 signature 参数中传输)。

此方法会打开一个带有支付网关的框架。

初始化 使用默认参数 (signed: false)。

返回 Promise<Purchase>

Purchase: object — 关于购买的信息。其包含以下属性:

  • productID: string — 产品 ID。
  • purchaseToken: string — 授权购买者使用购买的令牌。
  • developerPayload: string — 其他购买数据。

初始化 参数为 signed: true

返回 Promise<{ signature }>.

signature: string — 关于购买的加密数据和玩家认证签名

玩家成功进行购买后,Promise 会切换到“已完成”状态。如果玩家没有进行购买并关闭了窗口,Promise 状态将变为“已拒绝”。

注意

由于互联网连接不稳定,当玩家在进行购买时,可能会出现购买未在游戏中注册的情况。为避免出现这种情况,请使用检查未处理的购买处理购买并扣除游戏货币中描述的方法处理购买。

如果不遵守这些说明,可能导致应用的游戏内购买被禁用或被取消资格。

用户可以在未授权的情况下进行购买,但我们建议提前或在购买时建议他们进行授权

示例

一般:

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() 来:

  • 了解玩家已经完成了哪些购买;

  • 检查未处理的购买是否存在;

  • 处理持续性购买。

初始化 使用默认参数 (signed: false)。

返回 Promise<Purchase[]>.

Purchase[]: array — 玩家完成的购买列表。每一笔购买 Purchase 包含属性:

  • productID: string — 产品 ID。
  • 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 异常。
    });

初始化 参数为 signed: true

返回 Promise<{ signature }>.

signature: string — 购买的加密数据和用于验证玩家身份的签名。

示例

payments.getPurchases()
  .then(purchases => {
      fetch('https://your.game.server?purchase', {
          method: 'POST',
          headers: { 'Content-Type': 'text/plain' },
          body: purchases.signature
      });
  });

 

获取所有产品的目录

要获取可用购买项及其价格的列表,请使用方法 payments.getCatalog()

该方法会返回 Promise<Product[]>

Product[]: array — 提供给用户的产品列表。它根据开发者仪表板的 In-app purchases 选项卡中的表格生成。每个 Product 都有以下属性:

  • id: string — 产品 ID。
  • title: string — 产品名称。
  • description: string — 产品描述。
  • imageURI: string — 产品图片的 URL。
  • price: string — <price> <currency code> 格式的产品价格。例如。
  • priceValue: string — <price> 格式的产品价格。例如。
  • priceCurrencyCode: string — 货币代码。

Product.getPriceCurrencyImage(size) — 获取货币图标地址的方法。

  • size: string — 可选参数。可能的值包括:

    • small(默认)- 获取小图标。

    • medium — 获取中等大小的图标。

    • svg — 获取矢量图标。

此方法返回一个 string 类型的值 —— 图标的地址,例如 //yastatic.net/s3/games-static/static-data/images/payments/sdk/currency-icon-s@2x.png

示例

var gameShop = []
payments.getCatalog()
    .then(products => {
        gameShop = products;
    });

游戏内货币标识

要在游戏中显示游戏货币的名称和图标,请使用 SDK 的以下功能:

  • Product.price: string — 带有货币代码的价格。
  • Product.priceCurrencyCode: string — 货币代码。
  • Product.getPriceCurrencyImage() 返回 string — 货币图标的网址。

有关 Product 对象的更多详细信息,请参阅获取所有产品的目录部分。

处理购买并扣除游戏货币

有两种类型的购买:永久性(例如为了禁用广告的购买)和消耗性(例如消耗游戏货币的购买)。

要处理永久性购买,请使用 payments.getPurchases() 方法。

要处理消耗性购买,请使用 payments.consumePurchase() 方法。

payments.consumePurchase()

该方法会以“已完成”状态(如果处理成功)或“已拒绝”状态(如果发生错误)返回 Promise

注意

在调用 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() 检查未处理的购买(例如在每次启动游戏时)。

注意

在添加购买之前,请配置未处理支付检查。

这项检查对于通过审核是必需的,因此即使是对于测试购买,配置它也非常重要。如果在设置消费之前就在游戏中添加购买并对其进行测试,那么测试后可能会留下未处理的支付,这将使通过审核变得不可能。

初始化 使用默认参数 (signed: false)。

示例

payments.getPurchases().then(purchases => purchases.forEach(consumePurchase));

function consumePurchase(purchase) {
    if (purchase.productID === 'gold500') {
        player.incrementStats({ gold: 500 })
            .then(() => {
                payments.consumePurchase(purchase.purchaseToken)
            });
    }
}

初始化 参数为 signed: true

示例

payments.getPurchases()
    .then(purchases => {
        fetch('https://your.game.server?purchase', {
            method: 'POST',
            headers: { 'Content-Type': 'text/plain' },
            body: purchases.signature
        });
    });

 

欺诈防范

为了保护自己免受游戏中可能的刷分操作,应在服务器端处理购买:

  1. 初始化 YaGames.init()ysdk.getPayments() 时传入 { signed: true } 参数。
  2. payments.purchase()payments.getPurchases() 响应中获得的签名传送到您的服务器,并使用秘钥进行解密。
  3. 在您的服务器上为玩家充值游戏中获得的物品。
// 确保使用 { signed: true } 参数初始化购买

payments.purchase({ id: 'gold500' })
    .then(purchase => {
         // 购买成功!
        // 在服务器上添加 500 个金币...
        serverPurchase(purchase.signature); // 检查签名并为玩家充值数据。
    });

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

发送到服务器的请求中的 signature 参数包含购买数据和签名。两个字符串采用 base64 编码方式:<signature>.<JSON with the purchase data>

签名示例

hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=

传输的购买数据示例(JSON格式)

请注意,serverPurchase(signature) 函数使用的 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": "No ads",
      "description": "Disable ads in the game",
      "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);
    // 您可以在此处安全地应用购买。
  }
}

备注

技术支持团队将协助您将已完成的游戏发布到 Yandex 游戏平台。关于开发和测试方面的具体问题,其他开发人员将在Discord 频道中进行回答。

如果您遇到 Yandex Games SDK 方面的问题或有其他问题想要咨询,请联系支持部门:

发送电子邮件

id: string — 开发者仪表板中设置的产品 ID。

developerPayload: string — 可选参数。您想发送到服务器的其他购买数据(在 signature 参数中传输)。

发送到服务器的请求中的 signature 参数包含购买数据和签名。两个字符串采用 base64 编码方式:<signature>.<JSON with the purchase data>

purchaseToken: string — 由 payments.purchase()payments.getPurchases() 方法返回的令牌。

如果未通过 ysdk.getPayments() 初始化支付功能,请直接使用 ysdk.payments

上一篇
下一篇