如何正确恢复应用内购买?
How to properly restore an in-app purchase?
我的一些用户抱怨他们购买的应用内 "premium version" 没有恢复。
我联系了其中一位用户,并发送了一个包含其他消息的 APK。这是发生了什么:
- 当用户再次尝试购买商品时,会收到消息
Unable to buy item (response: 7: Item already owned)
。这是预期的消息,因为他已经购买了它。
- 但是当用户尝试恢复使用相同 SKU 的购买时,它 returns null/false。
这是购买的回调:
@Override
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
if (result.isFailure()) {
Log.d("debug", "failed - " + result.mMessage);
return;
}
Log.d("debug", "success");
// continue with the purchase validation...
}
这是恢复购买的回调:
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (result.isFailure()) {
Log.d("debug", "inventory: failed (" + result.mMessage + ")");
return;
}
if (inventory.hasPurchase(SKU_PREMIUM)) {
Log.d("debug", "success - purchase restored");
}
else {
Log.d("debug", "failure - no purchase found for this user");
}
}
请注意,只有少数用户会出现这种情况,我测试了几次,在我的测试中,我在查询库存后收到 success - purchase restored
消息。
需要说明的是,我使用的是 v3 API,并且此 SKU 是受管理的商品。我需要检查用户是否已经购买了它(我不想消费它)。
如果您使用的是应用内购买的 v3 版本,则版本 3 API 支持托管应用内产品 和订阅。
托管应用内商品是指其所有权信息由 Google Play 跟踪和管理的项目。当用户购买受管理的应用内商品时,Google Play 会为每个用户存储每件商品的购买信息。这使您可以在以后随时查询 Google Play 以恢复特定用户已购买的项目的状态。此信息在 Google Play 服务器上持久存在,即使用户卸载应用程序或更换设备也是如此。
如果您使用的是版本 3 API,您还可以在您的应用程序中使用托管 项目。您通常会为可以多次购买的物品(例如游戏内货币、燃料或魔法)实施消费。购买后,通过向 Google Play 发送消费请求,您无法再次购买受管理的项目,直到您消费了该项目。
消耗物品参考以下方法:
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
// if we were disposed of in the meantime, quit.
if (mHelper == null) return;
if (result.isFailure()) {
complain("Error purchasing: " + result);
return;
}
if (!verifyDeveloperPayload(purchase)) {
complain("Error purchasing. Authenticity verification failed.");
return;
}
Log.d(TAG, "Purchase successful.");
if (purchase.getSku().equals(SKU_PREMIUM)) {
//consumeItem();
Constant.showProgressDialog(getActivity());
mHelper.consumeAsync(purchase, mConsumeFinishedListener);
}
}
};
也将此方法添加到您的 activity
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
public void onConsumeFinished(Purchase purchase, IabResult result) {
Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
// if we were disposed of in the meantime, quit.
if (mHelper == null) return;
// We know this is the "gas" sku because it's the only one we consume,
// so we don't check which sku was consumed. If you have more than one
// sku, you probably should check...
if (result.isSuccess()) {
// successfully consumed, so we apply the effects of the item in our
// game world's logic, which in our case means filling the gas tank a bit
Log.d(TAG, "Consumption successful. Provisioning.");
}
else {
complain("Error while consuming: " + result);
}
Log.d(TAG, "End consumption flow.");
}
};
在您的 IabHelper.java class 中有以下方法:
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
// Query purchases
logDebug("Querying owned items, item type: " + itemType);
logDebug("Package name: " + mContext.getPackageName());
boolean verificationFailed = false;
String continueToken = null;
do {
logDebug("Calling getPurchases with continuation token: " + continueToken);
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
itemType, continueToken);
int response = getResponseCodeFromBundle(ownedItems);
logDebug("Owned items response: " + String.valueOf(response));
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getPurchases() failed: " + getResponseDesc(response));
return response;
}
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
logError("Bundle returned from getPurchases() doesn't contain required fields.");
return IABHELPER_BAD_RESPONSE;
}
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
RESPONSE_INAPP_ITEM_LIST);
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = ownedItems.getStringArrayList(
RESPONSE_INAPP_SIGNATURE_LIST);
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String signature = signatureList.get(i);
String sku = ownedSkus.get(i);
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
logDebug("Sku is owned: " + sku);
Purchase purchase = new Purchase(itemType, purchaseData, signature);
if (TextUtils.isEmpty(purchase.getToken())) {
logWarn("BUG: empty/null token!");
logDebug("Purchase data: " + purchaseData);
}
// Record ownership and token
inv.addPurchase(purchase);
}
else {
logWarn("Purchase signature verification **FAILED**. Not adding item.");
logDebug(" Purchase data: " + purchaseData);
logDebug(" Signature: " + signature);
verificationFailed = true;
}
}
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
logDebug("Continuation token: " + continueToken);
} while (!TextUtils.isEmpty(continueToken));
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
}
在此您将获得拥有的物品列表
很遗憾,这是我的错误。
受影响的用户在非常旧的版本上购买了应用内产品,对于相同的 SKU具有不同的开发人员负载,具体取决于购买在应用程序中的位置.
这些不同的开发人员有效负载已从最新版本中删除,这显然破坏了之前的购买。
我的一些用户抱怨他们购买的应用内 "premium version" 没有恢复。
我联系了其中一位用户,并发送了一个包含其他消息的 APK。这是发生了什么:
- 当用户再次尝试购买商品时,会收到消息
Unable to buy item (response: 7: Item already owned)
。这是预期的消息,因为他已经购买了它。 - 但是当用户尝试恢复使用相同 SKU 的购买时,它 returns null/false。
这是购买的回调:
@Override
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
if (result.isFailure()) {
Log.d("debug", "failed - " + result.mMessage);
return;
}
Log.d("debug", "success");
// continue with the purchase validation...
}
这是恢复购买的回调:
@Override
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (result.isFailure()) {
Log.d("debug", "inventory: failed (" + result.mMessage + ")");
return;
}
if (inventory.hasPurchase(SKU_PREMIUM)) {
Log.d("debug", "success - purchase restored");
}
else {
Log.d("debug", "failure - no purchase found for this user");
}
}
请注意,只有少数用户会出现这种情况,我测试了几次,在我的测试中,我在查询库存后收到 success - purchase restored
消息。
需要说明的是,我使用的是 v3 API,并且此 SKU 是受管理的商品。我需要检查用户是否已经购买了它(我不想消费它)。
如果您使用的是应用内购买的 v3 版本,则版本 3 API 支持托管应用内产品 和订阅。
托管应用内商品是指其所有权信息由 Google Play 跟踪和管理的项目。当用户购买受管理的应用内商品时,Google Play 会为每个用户存储每件商品的购买信息。这使您可以在以后随时查询 Google Play 以恢复特定用户已购买的项目的状态。此信息在 Google Play 服务器上持久存在,即使用户卸载应用程序或更换设备也是如此。
如果您使用的是版本 3 API,您还可以在您的应用程序中使用托管 项目。您通常会为可以多次购买的物品(例如游戏内货币、燃料或魔法)实施消费。购买后,通过向 Google Play 发送消费请求,您无法再次购买受管理的项目,直到您消费了该项目。
消耗物品参考以下方法:
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
// if we were disposed of in the meantime, quit.
if (mHelper == null) return;
if (result.isFailure()) {
complain("Error purchasing: " + result);
return;
}
if (!verifyDeveloperPayload(purchase)) {
complain("Error purchasing. Authenticity verification failed.");
return;
}
Log.d(TAG, "Purchase successful.");
if (purchase.getSku().equals(SKU_PREMIUM)) {
//consumeItem();
Constant.showProgressDialog(getActivity());
mHelper.consumeAsync(purchase, mConsumeFinishedListener);
}
}
};
也将此方法添加到您的 activity
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
public void onConsumeFinished(Purchase purchase, IabResult result) {
Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
// if we were disposed of in the meantime, quit.
if (mHelper == null) return;
// We know this is the "gas" sku because it's the only one we consume,
// so we don't check which sku was consumed. If you have more than one
// sku, you probably should check...
if (result.isSuccess()) {
// successfully consumed, so we apply the effects of the item in our
// game world's logic, which in our case means filling the gas tank a bit
Log.d(TAG, "Consumption successful. Provisioning.");
}
else {
complain("Error while consuming: " + result);
}
Log.d(TAG, "End consumption flow.");
}
};
在您的 IabHelper.java class 中有以下方法:
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
// Query purchases
logDebug("Querying owned items, item type: " + itemType);
logDebug("Package name: " + mContext.getPackageName());
boolean verificationFailed = false;
String continueToken = null;
do {
logDebug("Calling getPurchases with continuation token: " + continueToken);
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
itemType, continueToken);
int response = getResponseCodeFromBundle(ownedItems);
logDebug("Owned items response: " + String.valueOf(response));
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getPurchases() failed: " + getResponseDesc(response));
return response;
}
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
logError("Bundle returned from getPurchases() doesn't contain required fields.");
return IABHELPER_BAD_RESPONSE;
}
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
RESPONSE_INAPP_ITEM_LIST);
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = ownedItems.getStringArrayList(
RESPONSE_INAPP_SIGNATURE_LIST);
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String signature = signatureList.get(i);
String sku = ownedSkus.get(i);
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
logDebug("Sku is owned: " + sku);
Purchase purchase = new Purchase(itemType, purchaseData, signature);
if (TextUtils.isEmpty(purchase.getToken())) {
logWarn("BUG: empty/null token!");
logDebug("Purchase data: " + purchaseData);
}
// Record ownership and token
inv.addPurchase(purchase);
}
else {
logWarn("Purchase signature verification **FAILED**. Not adding item.");
logDebug(" Purchase data: " + purchaseData);
logDebug(" Signature: " + signature);
verificationFailed = true;
}
}
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
logDebug("Continuation token: " + continueToken);
} while (!TextUtils.isEmpty(continueToken));
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
}
在此您将获得拥有的物品列表
很遗憾,这是我的错误。
受影响的用户在非常旧的版本上购买了应用内产品,对于相同的 SKU具有不同的开发人员负载,具体取决于购买在应用程序中的位置.
这些不同的开发人员有效负载已从最新版本中删除,这显然破坏了之前的购买。