如何正确恢复应用内购买?

How to properly restore an in-app purchase?

我的一些用户抱怨他们购买的应用内 "premium version" 没有恢复。

我联系了其中一位用户,并发送了一个包含其他消息的 APK。这是发生了什么:

  1. 当用户再次尝试购买商品时,会收到消息 Unable to buy item (response: 7: Item already owned)。这是预期的消息,因为他已经购买了它。
  2. 但是当用户尝试恢复使用相同 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具有不同的开发人员负载,具体取决于购买在应用程序中的位置.

这些不同的开发人员有效负载已从最新版本中删除,这显然破坏了之前的购买。