Billing API v3 回调中 INAPP_PURCHASE_DATA 为 null
结论
Activity.RESULT_OK 只表示购买界面正常返回,并不代表购买成功。必须读取回调 Intent 中的 RESPONSE_CODE。只有它的值为 0(BILLING_RESPONSE_RESULT_OK)时,才能继续解析 INAPP_PURCHASE_DATA 和 INAPP_DATA_SIGNATURE。
如果用户已经拥有该 subscription,无论订阅仍在自动续订,还是已经取消自动续订但尚未到期,它通常都属于“已拥有”状态,不能再次走首次购买流程。此时可能返回 ITEM_ALREADY_OWNED(响应码 7),并且不会提供新的 INAPP_PURCHASE_DATA。
因此,不能只根据 resultCode == Activity.RESULT_OK 判断付款成功,也不能假定 INAPP_PURCHASE_DATA 一定存在。
处理购买回调
处理回调时,需要检查以下内容:
requestCode是否对应本次购买;data是否为null;RESPONSE_CODE是否为0;- 购买数据和签名是否存在;
- 返回的商品 ID、开发者载荷和签名是否符合预期。
private static final int REQUEST_CODE_BUY = 1001;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != REQUEST_CODE_BUY) {
return;
}
if (data == null) {
// 购买流程没有返回可解析的数据,不能视为购买成功
handlePurchaseError("Purchase result intent is null");
return;
}
int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
if (resultCode == Activity.RESULT_OK && responseCode == 0) {
String purchaseData =
data.getStringExtra("INAPP_PURCHASE_DATA");
String dataSignature =
data.getStringExtra("INAPP_DATA_SIGNATURE");
if (purchaseData == null || dataSignature == null) {
handlePurchaseError("Purchase data or signature is missing");
return;
}
// 在授予订阅权益前验证签名并校验购买内容
handleSuccessfulPurchase(purchaseData, dataSignature);
return;
}
if (responseCode == 1) {
handlePurchaseCancelled();
} else if (responseCode == 7) {
// ITEM_ALREADY_OWNED:通过 getPurchases() 恢复并确认现有订阅
restoreExistingSubscription();
} else {
handlePurchaseError(
"Billing failed: resultCode=" + resultCode
+ ", responseCode=" + responseCode);
}
}
不同 Play Store/Billing 实现返回的 RESPONSE_CODE 可能存在类型差异。为了兼容旧环境,可以同时处理 Integer 和 Long:
private int getResponseCodeFromIntent(Intent intent) {
Object value = intent.getExtras() != null
? intent.getExtras().get("RESPONSE_CODE")
: null;
if (value == null) {
return 0;
}
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Long) {
return (int) ((Long) value).longValue();
}
return -1;
}
发起购买前查询现有订阅
调用 getBuyIntent() 前,可以先用 getPurchases() 查询当前账户仍然拥有的 subscription。这样既可以避免重复拉起付款界面,也能恢复用户已经购买、但应用本地状态丢失的订阅。
Bundle purchases = mService.getPurchases(
3,
getPackageName(),
"subs",
null
);
int responseCode = purchases.getInt("RESPONSE_CODE");
if (responseCode == 0) {
ArrayList<String> ownedSkus =
purchases.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
ArrayList<String> purchaseDataList =
purchases.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
ArrayList<String> signatureList =
purchases.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
if (ownedSkus != null && ownedSkus.contains(mProduct)) {
// 用户仍拥有该订阅:验证购买数据后恢复订阅权益
} else {
// 未发现仍拥有的订阅,可以继续调用 getBuyIntent()
}
}
如果结果中包含 INAPP_CONTINUATION_TOKEN,还要携带该 token 继续查询,直到没有下一页,不能只检查第一页。
处理已取消但未到期的订阅
取消自动续订不会立即终止订阅权益。在当前计费周期结束前,用户通常仍然拥有该商品。因此,应用需要继续提供订阅权益,不能把“已取消自动续订”视为“可以重新购买”。
这笔订阅仍可能出现在 getPurchases() 的结果中。此时再次购买同一商品,可能得到 ITEM_ALREADY_OWNED,而不会生成新的购买记录。
只有在订阅真正到期后,它才会从当前已拥有的购买记录中消失。具体状态最好由可信的服务端校验确定,不要只依赖客户端缓存。
其他注意事项
不要把 startIntentSenderForResult() 返回的 Activity.RESULT_OK、付款窗口关闭或界面上的成功提示直接当作订阅已经生效。授予权益前,至少要确认:
- Billing
RESPONSE_CODE为0; INAPP_PURCHASE_DATA和INAPP_DATA_SIGNATURE都不为null;- 签名验证成功;
productId与发起购买的商品一致;developerPayload与发起购买时保存的值一致;- 如果对订阅状态的准确性要求较高,应通过服务端向 Google Play 验证购买令牌。
示例中的这一行缺少右括号和分号,实际代码应写成:
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
如果已经确认 RESPONSE_CODE == 0,但购买数据仍然为空,应将其视为异常或不完整的结果。记录 requestCode、resultCode、RESPONSE_CODE 和 Intent 中的键名,再通过 getPurchases() 恢复真实的购买状态。在确认状态前,不能直接授予订阅权益。
备注:内容仅供参考。
