PAYATHON 2026

如何在 Android 的 PhoneGap/Cordova 应用中销售数字商品?

支付老李

结论

可以实现,但 Cordova 没有与 blackberry.payment.* 一一对应的 API。Android 应用需要通过支持 Google Play Billing 的 Cordova 插件查询商品、购买商品和恢复交易。

项目中可以使用仍在维护的 Google Play Billing 插件,例如 cordova-plugin-purchase,也可以自行编写 Cordova 原生插件来封装 Google Play Billing Library。不要直接通过 JavaScript 调用 Google Play 接口,也不要绕过 Google Play 的购买流程,否则应用可能无法通过商店审核。

API 对应关系

BlackBerry 10 与 Google Play Billing 的概念大致对应如下:

BlackBerry APIAndroid/Google Play 中的操作
blackberry.payment.getPrice(options)查询商品详情,并读取本地化价格
blackberry.payment.getExistingPurchases()查询或恢复当前账号已有的有效购买
blackberry.payment.purchase(...)启动 Google Play 购买流程

迁移时不能只替换函数名。Google Play Billing 还需要处理购买确认、消耗型商品、未完成交易和服务端验签等流程。

实现步骤

1. 在 Google Play Console 创建商品

先在 Google Play Console 中创建商品,并确保商品 ID 与应用代码完全一致,例如:

premium_upgrade
coins_100
monthly_subscription

数字商品通常有三类:

  • 一次购买并永久有效的非消耗型商品,例如去广告或高级功能。
  • 可以重复购买的消耗型商品,例如金币或游戏道具。
  • 自动续订订阅,例如月度会员。

商品需要绑定正确的应用包名。测试时通常还要上传可供测试的构建版本,并配置许可测试账号或测试轨道。具体要求可能会随 Google Play Console 和 Billing Library 更新,应以当前官方控制台的提示为准。

2. 安装支持 Google Play Billing 的插件

以常用的 cordova-plugin-purchase 为例:

cordova plugin add cordova-plugin-purchase

插件不同主版本之间的 API 可能有明显差异。下面的代码只展示典型流程,实际属性名和回调方式应以项目锁定版本的文档为准,不要直接照搬旧教程。

3. 等待 Cordova 初始化

购买插件只能在 deviceready 触发后调用:

document.addEventListener("deviceready", initializePurchases, false);

function initializePurchases() {
    // 在这里注册商品并初始化购买系统
}

不要在普通网页的 DOMContentLoaded 事件中立即调用购买 API,此时 Cordova 原生桥接可能还没有准备好。

4. 注册商品并初始化商店

下面用较新的 cordova-plugin-purchase 编程模型说明基本结构:

function initializePurchases() {
    const {
        store,
        ProductType,
        Platform
    } = CdvPurchase;

    store.register([
        {
            id: "premium_upgrade",
            type: ProductType.NON_CONSUMABLE,
            platform: Platform.GOOGLE_PLAY
        },
        {
            id: "coins_100",
            type: ProductType.CONSUMABLE,
            platform: Platform.GOOGLE_PLAY
        }
    ]);

    store.error(error => {
        console.error("Billing error:", error);
    });

    store.when()
        .productUpdated(product => {
            updateProductUI(product);
        })
        .approved(transaction => {
            // 生产环境通常应先交给服务器验证。
            transaction.verify();
        })
        .verified(receipt => {
            grantPurchasedContent(receipt);
            receipt.finish();
        });

    store.initialize([Platform.GOOGLE_PLAY]);
}

收到 approved 事件并不代表可以无条件发放商品。更可靠的流程是先验证交易,再授予权益并完成交易。不同版本的插件可能采用不同的 verify()、finish() 和验证器配置方式,需要根据所用版本调整代码。

查询价格

Google Play 返回的价格已经按用户所在地区和币种完成格式化。界面应优先显示商店返回的价格字符串,不要自行拼接货币符号。

示意代码如下:

function updateProductUI(product) {
    const offer = product.getOffer?.();
    const pricingPhase = offer?.pricingPhases?.[0];

    if (!pricingPhase) {
        return;
    }

    const priceElement = document.querySelector(
        `[data-product-price="${product.id}"]`
    );

    if (priceElement) {
        priceElement.textContent = pricingPhase.price;
    }
}

价格字段的具体层级取决于插件版本和商品类型。订阅商品还可能有多个 offer、base plan 和 pricing phase,不能假设它始终只有一个价格。

发起购买

购买操作应由用户明确点击按钮后触发:

async function buyProduct(productId) {
    const { store, Platform } = CdvPurchase;

    const product = store.get(productId, Platform.GOOGLE_PLAY);
    const offer = product?.getOffer?.();

    if (!offer) {
        throw new Error(`Product is unavailable: ${productId}`);
    }

    const error = await offer.order();

    if (error) {
        console.error("Purchase failed:", error);
    }
}

最终的购买结果应通过交易事件处理,不能只根据 order() 是否返回错误来决定是否发放商品。应用可能被切换到后台,用户可能关闭购买窗口,交易也可能进入待处理状态。

恢复已有购买

Google Play Billing 初始化后,通常会同步当前账号拥有的有效购买。非消耗型商品和订阅可以通过这些数据恢复权益:

function restoreEntitlements() {
    const { store, Platform } = CdvPurchase;

    const premiumProduct = store.get(
        "premium_upgrade",
        Platform.GOOGLE_PLAY
    );

    if (premiumProduct?.owned) {
        enablePremiumFeatures();
    }
}

不要把 localStorage 中的标记当作购买凭证。用户可能更换设备、清除应用数据、退款或取消订阅,本地状态很容易过期。

消耗型商品的处理方式不同。商品完成消耗后,一般不会继续作为”当前拥有的购买”返回。因此,金币余额等业务数据应保存在可信的服务端,不能依靠恢复购买来重新计算。

服务端验证

如果商品具有实际价值,建议把购买凭证发送到自己的服务器,再由服务器调用 Google Play Developer API 或使用插件支持的验证服务完成校验:

async function sendPurchaseToServer(purchase) {
    const response = await fetch("/api/google-play/verify", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            productId: purchase.productId,
            purchaseToken: purchase.purchaseToken
        })
    });

    if (!response.ok) {
        throw new Error("Purchase verification failed");
    }

    return response.json();
}

服务端至少需要检查:

  • 包名是否属于自己的应用。
  • 商品 ID 是否与订单一致。
  • purchaseToken 是否有效,以及是否已被重复使用。
  • 订单当前处于已购买、撤销还是退款状态。
  • 订阅是否仍在有效期内。
  • 购买是否已经正确确认。

客户端传来的”购买成功”布尔值不能作为可信依据。

用户体验方面的建议

商品信息加载成功后再启用购买按钮,并显示 Google Play 返回的本地化价格。购买期间需要防止用户连续点击,但不要使用永久遮罩阻塞整个界面。

以下状态需要分别处理:

  • 用户主动取消购买。
  • 网络暂时不可用。
  • 商品尚未发布,或者测试账号无权购买。
  • 交易处于 pending 状态。
  • 用户已经拥有该非消耗型商品。
  • 购买成功,但服务器验证暂时失败。
  • 应用在交易完成前被关闭,之后重新启动。

遇到”付款成功但发货失败”时,不应要求用户再次付费。应用启动后应重新同步未完成的交易,并在验证成功后补发权益。

注意事项

Google Play 对应用分发数字内容所使用的支付方式有专门政策。一般情况下,应用内数字商品需要使用 Google Play Billing。实体商品、线下服务等场景可能适用不同规则。

还需要注意:

  • 不要硬编码显示价格。
  • 不要只凭客户端回调发放高价值商品。
  • 不要消耗非消耗型商品。
  • 消耗型商品只有在权益成功入账后才能完成或消耗。
  • 必须正确确认购买,否则 Google Play 可能自动退款。
  • 测试购买与正式购买应使用相同的状态处理逻辑。
  • 迁移旧项目时,需要确认插件是否兼容当前的 Cordova Android、Android SDK 和 Google Play Billing 要求。

更稳妥的做法不是寻找一个与 BlackBerry API 名称完全相同的插件,而是选择仍在积极维护的 Cordova Google Play Billing 插件,并按照”查询商品、发起购买、验证凭证、发放权益、完成交易、恢复购买”的完整流程重新组织支付代码。

备注:内容仅供参考。