跳转到内容

内购(IAP)

FRVR.iap 在 Google Play、Facebook Instant、Apple iAP 等渠道计费后端之上做抽象。你描述一份目录,各渠道会把你的产品 ID 映射到自家商店。

FRVR.config.iap = {
  catalog: {
    COINS_100:  { label: '100 金币',   storeId: 'com.mygame.coins_100' },
    COINS_500:  { label: '500 金币',   storeId: 'com.mygame.coins_500' },
    REMOVE_ADS: { label: '去广告',     storeId: 'com.mygame.remove_ads' },
  },
};
  • 键(COINS_100)是你自己代码里用的。
  • storeId 是当前渠道商店里这个商品的名字。
  • label 是 UI 上展示用的人类可读名(没有现成的就用它)。

FRVR.init() 之后、第一次引导 tick 之后,目录就绪:

const catalog = FRVR.iap.getCatalog();
// { COINS_100: { label, storeId, price, currency, … }, … }

const item = FRVR.iap.getProductById('COINS_100');
// { label, storeId, price, currency, description, … }

价格/币种来自底层商店。请用它们 —— 不要硬编码价格。

FRVR.iap.purchase('COINS_100', { meta: 'optional_game_context' })
  .then(purchase => grantCoins(100, purchase))
  .catch(err => {
    if (err instanceof FRVR.iap.errors.IAPPurchaseErrorCancelledByUser) return;
    if (err instanceof FRVR.iap.errors.IAPPurchaseErrorAlreadyOwned)    return ownedFlow();
    console.error('purchase failed', err);
  });

错误类型:

错误含义
IAPPurchaseErrorCancelledByUser用户关掉了支付窗口。这不是要弹给用户看的错误。
IAPPurchaseErrorAlreadyOwned该账号已拥有该耐用品。
IAPPurchaseErrorUnknownProduct产品 ID 错误。
IAPPurchaseErrorInProgress另一笔购买还在进行中。
IAPPurchaseErrorPopupBlocked浏览器拦截了结账窗口。没有扣款。
IAPPurchaseErrorPending玩家已付款,但支付尚未确认。不要发货。
IAPPurchaseErrorHeldByEconomy由 consumePurchase 抛出:FRVR 的 economy 仍在结算这笔购买。不要发货。
IAPError兜底通用错误。看 .code。

这些错误都继承自 IAPError,所以通用的 catch 照常可用。每个错误的 err.name 也等于它的类名;当错误可能来自另一个 bundle 时,用它判断更稳妥。

web 渠道通过 FRVR 的 Web 支付 provider 销售:Google Pay、Apple Pay 和银行卡结账。有多个可用时,SDK 会让玩家选择。和商店购买相比有几点不同:

  • **在点击或触摸的回调里直接调用 purchase()。**游戏运行在其他网站的 iframe 里时,结账会在新窗口打开;浏览器拦截它时会以 IAPPurchaseErrorPopupBlocked reject。请玩家再试一次。
  • **结账关闭时支付可能还未确认。**这时 purchase() 以 IAPPurchaseErrorPending reject(err.pending === true)。告诉玩家商品稍后到账。确认之后,这笔购买会从 getUnconsumedPurchases() 返回:在那里发货并消耗。
  • **玩家可能需要先登录。**SDK 会自己打开登录界面,然后继续购买。
  • 银行卡结账的价格含税。getProductById() 返回的这些商品带有 taxIncluded: true,玩家实付金额就是显示的价格。

要自己绘制支付按钮,先读取可选项,再把玩家选的那个传进去:

const options = FRVR.iap.getPurchaseOptions();
// [{ id, displayName, ready }, …]

await FRVR.iap.purchase('COINS_100', {}, { provider: options[0].id });

消耗品(金币、宝石)可以反复购买。发完物品后,要消耗这笔购买,商店才会放开让用户再次购买:

FRVR.iap.purchase('COINS_100')
  .then(purchase => {
    grantCoins(100);
    return FRVR.iap.consumePurchase(purchase);   // 返回 purchaseId
  });

耐用品(去广告、角色皮肤)永远不消耗。在新设备上重新发放:

FRVR.iap.restorePurchases().then(purchases => {
  for (const p of purchases) applyDurable(p.productId);
});

如果你的 App 在购买与发货之间崩了,商店会把收据保留为待处理状态。每次启动时:

FRVR.iap.getUnconsumedPurchases().then(purchases => {
  purchases.forEach(p => {
    grantCoins(lookupAmount(p.productId));
    FRVR.iap.consumePurchase(p);
  });
});

哪怕用户在购买中途强杀了 App,这一步也能把闭环兜回来。

如果 consumePurchase() 以 IAPPurchaseErrorHeldByEconomy reject,跳过这笔购买,不要发货。FRVR 的 economy 服务正在结算它,会在之后的启动中完成。

onReady 可能触发不止一次。玩家在会话中途登录了另一个账号时,SDK 会再调用它一次,让游戏恢复该账号的购买:

FRVR.iap.onReady(async (ready) => {
  if (!ready) return;
  for (const p of await FRVR.iap.restorePurchases()) applyDurable(p.productId);
});

确保这个回调执行两次也没问题。

dev 渠道自带一份模拟目录和一条假的购买流程。可以试一下买 → 消耗 → 恢复。

IAP:目录、购买、消耗、恢复

loading SDK…
 
方法作用
isReady() / onReady(cb)provider 是否就绪。账号变化后 onReady 会再次触发。
getCatalog()按你定义的 ID 列出所有商品。
getProductById(id)单个商品,含商店价格。
purchase(id, meta?, options?)发起结账。resolve 出收据。options.provider 可跳过支付方式选择。
getPurchaseOptions()玩家可选的支付方式。
consumePurchase(purchase)把消耗品标记为已发放。
restorePurchases()重新派发用户拥有的耐用品。
getUnconsumedPurchases()未完成的消耗品 —— 启动时调用以恢复。
errors.*用 instanceof 判定的错误类。