应用内购买 (IAP)

本指南讨论如何在 Solar2D 应用中实现应用内购买 (IAP)。

概述

应用内购买 (IAP) 允许用户从应用内购买额外内容。但是,此内容不能像实体库存一样通过市场交付 — 您必须在构建应用时将内容与其捆绑在一起,预期它会在购买后解锁/启用,或者您必须使用 网络 API 将其他内容下载到应用中。

产品类型

应用内应用内购买支持四种基本产品类型

  1. 用户只能购买一次的商品,例如付费解锁完整游戏、激活特殊玩家能力、解锁关卡20-40等。
  2. 用户可以多次购买的商品,例如宝石/硬币包、额外生命等。请注意,对于 Google IAP,这些商品必须在再次购买之前被消耗
  3. 可以按订阅方式购买的商品,自动续订,例如每月支付服务费来玩多人游戏。
  4. 可以按订阅方式购买的商品,不会自动续订,例如每年支付费用以查看信息应用中的高级内容。

每个商店的命名方式都不同。请参考下表

产品类型 Apple Google Amazon
一次性购买 非消耗型 托管商品 权限
多次购买 消耗型 带消耗的托管商品 消耗型
自动续订订阅 自动续订订阅 订阅 订阅
非续订订阅 非续订订阅    

产品标识符

产品由称为**产品标识符**的唯一字符串标识。每个商店可能使用不同的术语,但在所有情况下它都是一个字符串值,对于您的应用必须是唯一的。建议您在所有商店中遵循反向域名命名约定,例如

com.acmegames.SuperRunner.UnlockFullGame

在商店门户中配置产品时,产品标识符应命名清晰准确,因为您需要在 Solar2D 代码中使用它们来加载产品、提交购买请求并在交易完成后识别商品。此外,如果您打算支持多个商店/平台,则应在所有平台上为每个产品使用一致的名称 — 这将避免在整个 IAP 实现过程中需要额外的条件代码。

设置 / 配置

每个市场——Apple App Store、Google Play 和 Amazon Appstore——都有不同的应用内购买配置要求和设置程序。请针对您要定位的每个市场,按照以下步骤操作。

以下步骤适用于在 **iOS** 上进行应用内购买。

  1. 首先在 iTunes Connect 中配置您的银行和税务信息。应用内购买在完成这些步骤之前将**无法**进行,并且您不会收到错误消息报告。

  2. Apple Developer 门户中,创建一个唯一且完全限定的新 App ID(您**不能**使用通配符 App ID 用于使用应用内购买的应用)。如果您需要有关此过程的指导,请参阅此处

  3. 返回 iTunes Connect,使用与 Apple Developer 门户中的应用相同的**Bundle ID** 创建一个新应用。

  4. 最后,在 iTunes Connect 中配置您的应用内购买(产品)。此任务超出了本指南的范围,因此请参阅 Apple 的应用内购买配置指南以获取进一步的帮助。

以下步骤适用于在 **Android** 上进行应用内购买。

  1. 首先设置您的 Google 商家帐户并将其链接到 Google Play 开发者控制台应用内购买在完成这些步骤之前将**无法**进行,并且您不会收到错误消息报告。请参阅此处获取详细说明。

  2. 仍在控制台中时,创建一个新应用并填写发布应用所需的所有信息 — 但请注意,某些详细信息无需最终确定即可简单测试应用内购买。

  3. 配置您的应用内购买(产品)。此任务超出了本指南的范围,因此请参阅 Google 的管理应用内结算指南以获取进一步的帮助。

  4. 在 Solar2D 方面,通过在项目的 `build.settings` 文件的 `plugins` 表中添加条目来集成 Google Billing 插件

settings =
{
    plugins =
    {
        ["plugin.google.iap.billing"] =
        {
            publisherId = "com.coronalabs"
        },
    },
}
  1. 可选地,将 `license` 表添加到项目的 `config.lua` 文件中。在此表中,`key` 值应设置为相应的按应用Google Play 开发者控制台 获取的许可密钥。此密钥在盈利设置部分的盈利中指示。此密钥将允许插件直接在设备上以加密方式验证购买收据。
application =
{
    license =
    {
        google =
        {
            key = "YOUR_KEY",
        },
    },
}
  1. 当您准备好测试应用内购买时,构建您的应用以创建一个 `apk` 文件,该文件可以上传到 Google Play 开发者控制台。然后,继续 Google 的测试应用内结算指南。

以下步骤适用于在 **Amazon** 上进行应用内购买。

  1. 如果您还没有,请注册一个 Amazon Developer 帐户。

  2. 如果您不熟悉Amazon 应用内购买,请阅读 Amazon 的了解应用内购买指南。

  3. 配置您的应用内购买(产品)。此任务超出了本指南的范围,因此请参阅 Amazon 的提交 IAP 商品指南以获取进一步的帮助。

  4. 在 Solar2D 方面,通过在项目的 `build.settings` 文件的 `plugins` 表中添加条目来集成 Amazon IAP 插件

settings =
{
    plugins =
    {
        ["plugin.amazon.iap"] =
        {
            publisherId = "com.coronalabs"
        },
    },
}
  1. 最后,安装 Amazon App Tester 或在 Amazon Appstore 中发布您的应用。有关测试的详细信息,请参见此处

初始化 IAP

模块包含

由于每个 IAP 提供商在 Solar2D 端使用不同的模块/插件,因此您必须加载正确的模块/插件。

如果您只支持一个商店,您可以简单地使用 `require()` 加载相应的模块,如下所示

local store = require( "store" )  -- iOS
local store = require( "plugin.google.iap.billing" )  -- Android
local store = require( "plugin.amazon.iap" )  -- Amazon

或者,如果您要支持多个平台,则可以实现条件例程。在以下示例中,使用 system.getInfo() 调用来检测构建的应用将面向哪个商店,并使用该属性来加载相应的模块。

local store

local targetAppStore = system.getInfo( "targetAppStore" )

if ( "apple" == targetAppStore ) then  -- iOS
    store = require( "store" )
elseif ( "google" == targetAppStore ) then  -- Android
    store = require( "plugin.google.iap.billing" )
elseif ( "amazon" == targetAppStore ) then  -- Amazon
    store = require( "plugin.amazon.iap" )
else
    print( "In-app purchases are not available for this platform." )
end

在为基于 Android 的设备(包括 Amazon 的 Kindle Fire 设备)构建应用时,请记住从 Solar2D 构建对话框窗口(指南)中选择正确的**目标应用商店**。

初始化

加载正确的模块后,您必须使用 `store.init()` 调用对其进行初始化

store.init( transactionListener )

唯一必需的参数 `transactionListener` 是您打算用于处理商店**交易**请求的函数,包括购买和潜在退款。例如

local function transactionListener( event )

    local transaction = event.transaction

    if ( transaction.isError ) then
        print( transaction.errorType )
        print( transaction.errorString )
    else
        -- No errors; proceed
    end
end

-- Initialize store
store.init( transactionListener )

`store.init()` 调用是必需的,并且必须在您尝试调用任何其他与商店相关的函数之前执行。

对于 Google IAP,此相同的交易侦听器函数还处理初始化(init)事件。因此,如果您使用的是 Google IAP,则应通过有条件地检查 event.name 的值来区分商店交易事件和初始化事件。有关执行此操作的示例,请参阅 Google IAP store.init() 文档。

调用 `store.init()` 后,您可以检查 `store.isActive` 属性以确认商店已成功初始化(值为 `true`)。

加载产品

在您的 Solar2D 应用中,您可以使用 `store.loadProducts()` 函数加载您在相应商店中输入的产品信息

store.loadProducts( productIdentifiers, productListener )

此函数需要以下两个参数

local productIdentifiers = {
    "com.domainname.testProduct1",
    "com.domainname.testProduct2",
    "com.domainname.testProduct3",
}
local productIdentifiers = {
    "com.domainname.testProduct1",
    "com.domainname.testProduct2",
    "com.domainname.testProduct3",
}

local function productListener( event )

end

-- Load store products; store must be properly initialized by this point!
store.loadProducts( productIdentifiers, productListener )

如果您为所有平台保持产品标识符一致,则可以使用一个产品数组,但如果您的标识符因商店而异,则需要使用单独的数组和条件逻辑与`system.getInfo("targetAppStore")`一起加载相应商店的相应产品。

产品属性

加载产品后,产品侦听器函数(`store.loadProducts()` 的 `productListener` 参数)将接收一个 `event` 表作为其唯一参数。由于商店功能的核心差异,与此表关联的属性/键会略有不同,但至少会有以下两个属性可用

  • `event.products` — 一个表,其中每个元素是另一个包含详细产品信息的表。这主要是您需要检查以获取有关已加载产品的详细信息的表。

  • `event.invalidProducts` — 一个表,其中每个元素都是一个表示产品标识符的字符串。此表将仅由不存在或不可用的产品填充。

通常,您应该遍历(循环)`event.products` 以确定哪些产品可用。例如

local productIdentifiers = {
    "com.domainname.testProduct1",
    "com.domainname.testProduct2",
    "com.domainname.testProduct3",
}

local function productListener( event )

    for i = 1,#event.products do
        print( event.products[i].productIdentifier )
    end
end

-- Load store products; store must be properly initialized by this point!
store.loadProducts( productIdentifiers, productListener )

对于 `event.products` 中的每个实例,都会提供各种属性,这些属性可用于创建界面(商店场景)或以其他方式显示可用的产品。由于商店功能的核心差异,这些属性会因商店而异,但以下通用属性适用于所有商店的产品:

  • productIdentifier — 表示产品标识符的字符串
  • title — 表示产品标题的字符串
  • description — 表示产品描述的字符串
  • localizedPrice — 产品价格,以本地化货币字符串表示,例如 `$0.99`。

以上属性应该足以向用户/玩家显示一个信息丰富的产品列表,但您可能需要查阅iOSAndroidAmazon的文档,以查看其他属性。

处理交易

当用户/玩家选择在您的应用内购买产品时,将与商店的服务器建立连接以启动交易。然后,商店将处理交易并将结果数据发送回您的应用。如上所述,此事件由交易侦听器函数处理(`store.init()` 中的 `transactionListener` 参数).

您的交易侦听器函数应处理以下所有情况:

交易属性

发生交易时,分派给交易侦听器函数的 `event.transaction` 表中将提供各种属性。由于商店功能的核心差异,这些属性会因商店而异,但以下通用属性适用于所有商店的交易:

  • state — 指示交易状态的字符串,例如 `"purchased"`、`"cancelled"` 或 `"failed"`。
  • identifier — 交易的唯一字符串标识符。
  • productIdentifier — 表示与交易关联的产品标识符的字符串
  • receipt — 交易收据的 JSON 格式字符串表示形式。
  • date — 表示交易发生日期的字符串
  • isError布尔值,指示是否发生错误。如果为 `true`,则 `errorType` 和 `errorString` 将是说明原因的字符串
  • errorType — 如果 `isError` 为 `true`,则表示发生的错误类型的字符串
  • errorString — 如果 `isError` 为 `true`,则为更详细的错误消息(字符串)。

收到交易事件后,您应该采取适当的行动。例如,如果用户成功购买了一件商品,您可以将此信息保存到文件或本地数据库中,并解锁使用该商品的功能。然后,可以在以后的会话中引用此文件,以便您知道该商品已被购买。

以下是交易侦听器函数的示例框架:

local function transactionListener( event )

    local transaction = event.transaction

    if ( transaction.isError ) then
        print( transaction.errorType )
        print( transaction.errorString )
    else
        -- No errors; proceed
        if ( transaction.state == "purchased" or transaction.state == "restored" ) then
            -- Handle a normal purchase or restored purchase here
            print( transaction.state )
            print( transaction.productIdentifier )
            print( transaction.date )

        elseif ( transaction.state == "cancelled" ) then
            -- Handle a cancelled transaction here

        elseif ( transaction.state == "failed" ) then
            -- Handle a failed transaction here
        end

        -- Tell the store that the transaction is complete
        -- If you're providing downloadable content, do not call this until the download has completed
        store.finishTransaction( transaction )
    end
end

-- Initialize store
store.init( transactionListener )

对于 Google IAP,此相同的交易侦听器函数还处理初始化(init)事件。因此,如果您使用的是 Google IAP,则应通过有条件地检查 event.name 的值来区分商店交易事件和初始化事件。有关执行此操作的示例,请参阅 Google IAP store.init() 文档。

如上所述,交易完成后,您必须在交易对象上调用 `store.finishTransaction()`。如果不这样做,商店会认为交易被中断,并会在下次应用程序启动时尝试恢复它。

        end

        -- Tell the store that the transaction is complete
        -- If you're providing downloadable content, do not call this until the download has completed
        store.finishTransaction( transaction )
    end
end

如上所述,由于商店功能的核心差异,返回的属性和值会略有不同。例如,Apple 将为已恢复购买的 `state` 属性返回 `"restored"`,但 Google Play 和 Amazon 会将普通购买和已恢复购买统称为 `"purchased"` 状态。此外,每个商店都会返回一系列您可能需要检查的唯一属性,因此请分别查阅iOSAndroidAmazon的文档。

购买产品

要启动购买,请使用 `store.purchase()` 函数。调用此函数时,它将向商店提交购买请求,当商店完成交易处理后,将在 `store.init()` 中指定的侦听器函数将被调用。

store.purchase( productIdentifier )

如上面的交易属性中所述,交易侦听器函数中的 `event.transaction` 将收到 `state` 属性为 `"purchased"`,表示购买已成功处理。

恢复已购商品

如果用户清除设备上的信息或购买新设备,他/她需要一种方法来恢复以前从您的应用购买的商品(无需再次付费)。`store.restore()` 函数启动此过程

store.restore()

调用此函数时,将启动恢复过程,在此过程中,交易侦听器函数可能会被多次调用(每件商品一次)。然后,您可以使用每个产品标识符(`event.transaction.productIdentifier`),使用适合您应用的任何集成将这些产品重置为“已拥有”或“已解锁”。

如上所述,Apple 将为已恢复购买的 `state` 属性返回 `"restored"`,但 Google Play 和 Amazon 会将普通购买和已恢复购买统称为 `"purchased"` 状态。通常,您可以像上面交易属性中所示的示例一样,使用 `or` 运算符来处理这两种情况。

local transaction = event.transaction

if ( transaction.state == "purchased" or transaction.state == "restored" ) then
    -- Handle a normal purchase or restored purchase here

商店特定功能

每个市场都提供一些独特且可能至关重要的功能,您必须了解这些功能。以下列表总结了这些功能,但您应始终查阅iOSAndroidAmazon的文档,以检查特定平台的详细功能。

禁用购买 (Apple)

iOS 设备有一个可以禁用应用内所有购买的设置。这通常用于防止儿童在未经许可的情况下意外购买商品。对于 Apple IAP,Solar2D 提供了 store.canMakePurchases 属性来检查购买是否已启用或禁用。您应该使用此属性提前检查是否允许购买,并在禁止购买时通知用户。

消耗商品 (Google)

Google IAP 要求您消耗购买的商品,才能再次购买该商品。本质上,一旦产品被购买,它就被视为“已拥有”,并且不能再次购买。但是,由于您几乎肯定希望鼓励玩家再次购买某些商品(例如宝石/硬币包、额外的生命等),因此您必须发送消耗请求将“已拥有”的产品恢复为“未拥有”产品,以便它们可以再次购买。消耗产品还会丢弃其以前的购买数据。

要消耗商品,请使用关联的产品标识符调用 store.consumePurchase()

store.consumePurchase( productIdentifier )

调用后,并在收到成功的消耗事件后,交易侦听器函数中 `event.transaction.state` 的值将为 `"consumed"`。此时,您可以重新启用用户界面或商店场景中的产品购买。

请注意,某些商品设计为只能购买一次,您不应消耗它们。例如,如果购买解锁了游戏中的新世界或授予角色永久性的能力提升,则它应该不符合未来购买的条件。

处理退款 (Google)

Google IAP 允许退款交易(说明)。收到成功的退款事件后,交易侦听器函数中 `event.transaction.state` 的值将为 `"refunded"`,在这种情况下,您可以禁用已退款的内容和/或将其从本地应用中删除(如果已下载)。

沙盒模式 (Amazon)

Amazon 允许您测试应用内通过“沙盒”模式进行购买,在这种模式下不会进行实际购买。如果您想在 Solar2D 应用中实现某种形式的调试,可以使用 store.isSandboxMode() API 检查应用当前是否处于测试模式。