Google Play计费API:如何了解用户已订阅? [英] Google play billing API: How to understand the user is subscribed?

本文介绍了Google Play计费API:如何了解用户已订阅?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想确定用户是否从MainActivity主动订阅了Basic/Premium内容.有一个BillingClientLifecycle类可启动订阅过程.据我了解,queryPurchses应该显示用户是否具有活动订阅.但是显然,它显示(通过Toasts,我放置在此处以显示订阅状态),即使实际上尚未订阅该用户,也已订阅该用户.

I want to find out whether the user active subscription to Basic/Premium content or not from the MainActivity. There is a BillingClientLifecycle class initiating the subscription process. As I understood, queryPurchses should show whether the user has active subscription or not. But apparently it shows (by the Toasts that I put there to show the subscription status) the user is subscribed even when the user is actually not subscribed.

public void queryPurchases() {
        if (!billingClient.isReady()) {
            Log.e(TAG, "queryPurchases: BillingClient is not ready");
        }
        Log.d(TAG, "queryPurchases: SUBS");
        Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
        if (result == null) {
            Log.i(TAG, "queryPurchases: null purchase result");
            processPurchases(null);
            ///
            Toast.makeText(applicationContext,"queryPurchases: null purchase result", Toast.LENGTH_SHORT).show();
        } else {
            if (result.getPurchasesList() == null) {
                Log.i(TAG, "queryPurchases: null purchase list");
                processPurchases(null);
                ///
                Toast.makeText(applicationContext,"queryPurchases: null purchase list", Toast.LENGTH_SHORT).show();
            } else {
                processPurchases(result.getPurchasesList());
                ///
                Toast.makeText(applicationContext,"user has subscription!", Toast.LENGTH_SHORT).show();
            }
        }
    }

我在这里做错了什么?我想根据订阅状态更新主要活动. BillingClientLifecycle如下:

What am I doing wrong here? I want to update the main activity according to the subscription status. The BillingClientLifecycle is as below:

public class BillingClientLifecycle implements LifecycleObserver, PurchasesUpdatedListener,
    BillingClientStateListener, SkuDetailsResponseListener {

private static final String TAG = "BillingLifecycle";

Context applicationContext = MainActivity.getContextOfApplication();

/**
 * The purchase event is observable. Only one observer will be notified.
 */
public SingleLiveEvent<List<Purchase>> purchaseUpdateEvent = new SingleLiveEvent<>();

/**
 * Purchases are observable. This list will be updated when the Billing Library
 * detects new or existing purchases. All observers will be notified.
 */
public MutableLiveData<List<Purchase>> purchases = new MutableLiveData<>();

/**
 * SkuDetails for all known SKUs.
 */
public MutableLiveData<Map<String, SkuDetails>> skusWithSkuDetails = new MutableLiveData<>();

private static volatile BillingClientLifecycle INSTANCE;

private Application app;
private BillingClient billingClient;

public BillingClientLifecycle(Application app) {
    this.app = app;
}

public static BillingClientLifecycle getInstance(Application app) {
    if (INSTANCE == null) {
        synchronized (BillingClientLifecycle.class) {
            if (INSTANCE == null) {
                INSTANCE = new BillingClientLifecycle(app);
            }
        }
    }
    return INSTANCE;
}

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
public void create() {
    Log.d(TAG, "ON_CREATE");
    // Create a new BillingClient in onCreate().
    // Since the BillingClient can only be used once, we need to create a new instance
    // after ending the previous connection to the Google Play Store in onDestroy().
    billingClient = BillingClient.newBuilder(app)
            .setListener(this)
            .enablePendingPurchases() // Not used for subscriptions.
            .build();
    if (!billingClient.isReady()) {
        Log.d(TAG, "BillingClient: Start connection...");
        billingClient.startConnection(this);
    }
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void destroy() {
    Log.d(TAG, "ON_DESTROY");
    if (billingClient.isReady()) {
        Log.d(TAG, "BillingClient can only be used once -- closing connection");
        // BillingClient can only be used once.
        // After calling endConnection(), we must create a new BillingClient.
        billingClient.endConnection();
    }
}

@Override
public void onBillingSetupFinished(BillingResult billingResult) {
    int responseCode = billingResult.getResponseCode();
    String debugMessage = billingResult.getDebugMessage();
    Log.d(TAG, "onBillingSetupFinished: " + responseCode + " " + debugMessage);
    if (responseCode == BillingClient.BillingResponseCode.OK) {
        // The billing client is ready. You can query purchases here.
        querySkuDetails();
        queryPurchases();
    }
}

@Override
public void onBillingServiceDisconnected() {
    Log.d(TAG, "onBillingServiceDisconnected");
    // TODO: Try connecting again with exponential backoff.
}

/**
 * Receives the result from {@link #querySkuDetails()}}.
 * <p>
 * Store the SkuDetails and post them in the {@link #skusWithSkuDetails}. This allows other
 * parts of the app to use the {@link SkuDetails} to show SKU information and make purchases.
 */
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
    if (billingResult == null) {
        Log.wtf(TAG, "onSkuDetailsResponse: null BillingResult");
        return;
    }

    int responseCode = billingResult.getResponseCode();
    String debugMessage = billingResult.getDebugMessage();
    switch (responseCode) {
        case BillingClient.BillingResponseCode.OK:
            Log.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
            if (skuDetailsList == null) {
                Log.w(TAG, "onSkuDetailsResponse: null SkuDetails list");
                skusWithSkuDetails.postValue(Collections.<String, SkuDetails>emptyMap());
            } else {
                Map<String, SkuDetails> newSkusDetailList = new HashMap<String, SkuDetails>();
                for (SkuDetails skuDetails : skuDetailsList) {
                    newSkusDetailList.put(skuDetails.getSku(), skuDetails);
                }
                skusWithSkuDetails.postValue(newSkusDetailList);
                Log.i(TAG, "onSkuDetailsResponse: count " + newSkusDetailList.size());
            }
            break;
        case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
        case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
        case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
        case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
        case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
        case BillingClient.BillingResponseCode.ERROR:
            Log.e(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
            break;
        case BillingClient.BillingResponseCode.USER_CANCELED:
            Log.i(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
            break;
        // These response codes are not expected.
        case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
        case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
        case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
        default:
            Log.wtf(TAG, "onSkuDetailsResponse: " + responseCode + " " + debugMessage);
    }
}

/**
 * Query Google Play Billing for existing purchases.
 * <p>
 * New purchases will be provided to the PurchasesUpdatedListener.
 * You still need to check the Google Play Billing API to know when purchase tokens are removed.
 */
public void queryPurchases() {
    if (!billingClient.isReady()) {
        Log.e(TAG, "queryPurchases: BillingClient is not ready");
    }
    Log.d(TAG, "queryPurchases: SUBS");
    Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
    if (result == null) {
        Log.i(TAG, "queryPurchases: null purchase result");
        processPurchases(null);
        ///
        Toast.makeText(applicationContext,"queryPurchases: null purchase result", Toast.LENGTH_SHORT).show();
    } else {
        if (result.getPurchasesList() == null) {
            Log.i(TAG, "queryPurchases: null purchase list");
            processPurchases(null);
            ///
            Toast.makeText(applicationContext,"queryPurchases: null purchase list", Toast.LENGTH_SHORT).show();
        } else {
            processPurchases(result.getPurchasesList());
            ///
            Toast.makeText(applicationContext,"user has subscription!", Toast.LENGTH_SHORT).show();
        }
    }
}

/**
 * Called by the Billing Library when new purchases are detected.
 */
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
    if (billingResult == null) {
        Log.wtf(TAG, "onPurchasesUpdated: null BillingResult");
        return;
    }
    int responseCode = billingResult.getResponseCode();
    String debugMessage = billingResult.getDebugMessage();
    Log.d(TAG, "onPurchasesUpdated: $responseCode $debugMessage");
    switch (responseCode) {
        case BillingClient.BillingResponseCode.OK:
            if (purchases == null) {
                Log.d(TAG, "onPurchasesUpdated: null purchase list");
                processPurchases(null);
            } else {
                processPurchases(purchases);
            }
            break;
        case BillingClient.BillingResponseCode.USER_CANCELED:
            Log.i(TAG, "onPurchasesUpdated: User canceled the purchase");
            break;
        case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
            Log.i(TAG, "onPurchasesUpdated: The user already owns this item");
            break;
        case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
            Log.e(TAG, "onPurchasesUpdated: Developer error means that Google Play " +
                    "does not recognize the configuration. If you are just getting started, " +
                    "make sure you have configured the application correctly in the " +
                    "Google Play Console. The SKU product ID must match and the APK you " +
                    "are using must be signed with release keys."
            );
            break;
    }
}

/**
 * Send purchase SingleLiveEvent and update purchases LiveData.
 * <p>
 * The SingleLiveEvent will trigger network call to verify the subscriptions on the sever.
 * The LiveData will allow Google Play settings UI to update based on the latest purchase data.
 */
private void processPurchases(List<Purchase> purchasesList) {
    if (purchasesList != null) {
        Log.d(TAG, "processPurchases: " + purchasesList.size() + " purchase(s)");
    } else {
        Log.d(TAG, "processPurchases: with no purchases");
    }
    if (isUnchangedPurchaseList(purchasesList)) {
        Log.d(TAG, "processPurchases: Purchase list has not changed");
        return;
    }
    purchaseUpdateEvent.postValue(purchasesList);
    purchases.postValue(purchasesList);
    if (purchasesList != null) {
        logAcknowledgementStatus(purchasesList);
    }
}

/**
 * Log the number of purchases that are acknowledge and not acknowledged.
 * <p>
 * https://developer.android.com/google/play/billing/billing_library_releases_notes#2_0_acknowledge
 * <p>
 * When the purchase is first received, it will not be acknowledge.
 * This application sends the purchase token to the server for registration. After the
 * purchase token is registered to an account, the Android app acknowledges the purchase token.
 * The next time the purchase list is updated, it will contain acknowledged purchases.
 */
private void logAcknowledgementStatus(List<Purchase> purchasesList) {
    int ack_yes = 0;
    int ack_no = 0;
    for (Purchase purchase : purchasesList) {
        if (purchase.isAcknowledged()) {
            ack_yes++;
        } else {
            ack_no++;
        }
    }
    Log.d(TAG, "logAcknowledgementStatus: acknowledged=" + ack_yes +
            " unacknowledged=" + ack_no);
}

/**
 * Check whether the purchases have changed before posting changes.
 */
private boolean isUnchangedPurchaseList(List<Purchase> purchasesList) {
    // TODO: Optimize to avoid updates with identical data.
    return false;
}

/**
 * In order to make purchases, you need the {@link SkuDetails} for the item or subscription.
 * This is an asynchronous call that will receive a result in {@link #onSkuDetailsResponse}.
 */
public void querySkuDetails() {
    Log.d(TAG, "querySkuDetails");

    List<String> skus = new ArrayList<>();
    skus.add(Constants.BASIC_SKU);
    skus.add(Constants.PREMIUM_SKU);

    SkuDetailsParams params = SkuDetailsParams.newBuilder()
            .setType(BillingClient.SkuType.SUBS)
            .setSkusList(skus)
            .build();

    Log.i(TAG, "querySkuDetailsAsync");
    billingClient.querySkuDetailsAsync(params, this);
}

/**
 * Launching the billing flow.
 * <p>
 * Launching the UI to make a purchase requires a reference to the Activity.
 */
public int launchBillingFlow(Activity activity, BillingFlowParams params) {
    String sku = params.getSku();
    String oldSku = params.getOldSku();
    Log.i(TAG, "launchBillingFlow: sku: " + sku + ", oldSku: " + oldSku);
    if (!billingClient.isReady()) {
        Log.e(TAG, "launchBillingFlow: BillingClient is not ready");
    }
    BillingResult billingResult = billingClient.launchBillingFlow(activity, params);
    int responseCode = billingResult.getResponseCode();
    String debugMessage = billingResult.getDebugMessage();
    Log.d(TAG, "launchBillingFlow: BillingResponse " + responseCode + " " + debugMessage);
    return responseCode;
}

/**
 * Acknowledge a purchase.
 * <p>
 * https://developer.android.com/google/play/billing/billing_library_releases_notes#2_0_acknowledge
 * <p>
 * Apps should acknowledge the purchase after confirming that the purchase token
 * has been associated with a user. This app only acknowledges purchases after
 * successfully receiving the subscription data back from the server.
 * <p>
 * Developers can choose to acknowledge purchases from a server using the
 * Google Play Developer API. The server has direct access to the user database,
 * so using the Google Play Developer API for acknowledgement might be more reliable.
 * TODO(134506821): Acknowledge purchases on the server.
 * <p>
 * If the purchase token is not acknowledged within 3 days,
 * then Google Play will automatically refund and revoke the purchase.
 * This behavior helps ensure that users are not charged for subscriptions unless the
 * user has successfully received access to the content.
 * This eliminates a category of issues where users complain to developers
 * that they paid for something that the app is not giving to them.
 */
public void acknowledgePurchase(String purchaseToken) {
    Log.d(TAG, "acknowledgePurchase");
    AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchaseToken)
            .build();
    billingClient.acknowledgePurchase(params, new AcknowledgePurchaseResponseListener() {
        @Override
        public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
            int responseCode = billingResult.getResponseCode();
            String debugMessage = billingResult.getDebugMessage();
            Log.d(TAG, "acknowledgePurchase: " + responseCode + " " + debugMessage);
        }
    });
}

}

我正在考虑在BillingClientLifecycle类中使用共享的首选项(而不是Toasts),并从MainActivity类或需要在启动应用程序时通知订阅状态的任何其他类中检索订阅状态.尽管我不想使用共享的首选项,而是直接调用订阅信息.

I am thinking of using shared preferences (instead of the Toasts) inside the BillingClientLifecycle class and retrieve the subscription status from the MainActivity class or any other classes the requires to be notified of the subscription status when the app is launched. Although I prefer not to use the shared preferences and directly call for the subscription info.

推荐答案

计费过程的实现看起来不错,但是缺少检查来确定订阅当前是否处于活动状态.

The implementation of the billing process looks good, but missing a check to determine whether the subscription is really active at the current moment.

可以使用LiveData对象进行观察.这样我们就不需要SharedPreferences左右的状态.我将在下面的观察部分进行介绍.详细的答案:

Observing can be done by using LiveData objects. So that we do not need the SharedPreferences or so to hold the state. I'll cover this at the observing part below. A detailed answer:

购买清单

首先让我们解释什么此处的购买清单在计费API中的确切含义:

Let's first explain what the purchases list here exactly means in the billing API:

  1. 这是用户所有购买的应用内商品或订阅的列表.
  2. 这些购买必须由应用程序或后端确认(通过后端推荐,但两者均可)
  3. 此购买列表包括仍处于待处理状态的付款,以及尚未确认的付款.
  1. This is the list of all the purchases the user has for an in-app item or subscription.
  2. These purchases have to be acknowledged by either the app or the backend (recommended via the backend, but both are possible)
  3. This purchases list includes payments which are still pending and also the payments that are not acknowledged yet.

看到确认步骤已执行,我认为付款确认已成功完成.

Seeing the acknowledge step being implemented, I assume the acknowledgement of the payment is done successfully.

第3点是为什么它不检测实际订阅状态的原因,因为未检查购买状态.

Point 3 is why it doesn't detect the actual subscribed state, as the state of the purchases aren't checked.

检查订阅状态

queryPurchases()调用返回用户对所请求产品的付款.我们收到的数组可以有多个项目(每个应用内项目或订阅最多可以有一个项目).我们需要检查所有这些.

The queryPurchases() call returns the payments of the user for the requested products. The array that we receive back can have multiple items (mostly one per in-app item or subscription). We need to check them all.

每次购买都有更多数据.这是我们检查状态所需的方法:

Each purchase has some more data. Here are the methods that we need for checking the state:

  • getSku()//验证产品是否是我们想要的
  • getPurchaseState()//获取实际的购买状态
  • isAcknowledged()//要知道付款是否已确认,如果未确认,则表示付款尚未成功
  • getSku() // To verify the product is what we want
  • getPurchaseState() // To get the actual purchase status
  • isAcknowledged() // To know if the payment is acknowledged, if not, it means that the payment is not successful yet

为了检查当前是否已为PREMIUM sku 付款并有效:

In order to check whether a purchase is currently paid and active for the PREMIUM sku:

boolean isPremiumActive = Constants.PREMIUM_SKU.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()

如果我们要检查是否有任何订阅处于活动状态,我们将检查其他sku是否相同(遍历sku和购买)

If we want to check if any of the subscriptions is active, we check for the other sku's the same (looping through the sku's and purchases)

*请注意,现在如果isPremiumActive为true,则意味着用户当前具有有效的订阅.这意味着,如果用户取消了他的订阅,但直到结束期仍付款,则该值仍为true.仅仅是因为用户仍然有权在计费周期到期之前访问内容.

* Note that now if isPremiumActive is true, it means that the user currently has an active subscription. This means that if the user canceled his subscription but still has paid till the ending period, this value will still be true. Simply because the user has still the right to access the content until the expiration of the billing period.

*如果订阅期确实结束(取消或过期),则计费客户端将不再退还购买的商品.

观察当前状态

现在,我们知道如何验证购买了,我们可以使用LiveData轻松读取此状态,以便我们可以随时访问它.在该示例中,我们已经有LiveData purchases,其中包含所有购买的商品,并在queryPurchases()调用后填充.

Now that we know how to verify the purchases, we can read this state easily by using LiveData so that we can access it anytime. In the example we already have te LiveData purchases, this one contains all the purchases and is filled after the queryPurchases() call.

  1. 创建LiveData

让我们创建一个使用此purchases LiveData的新LiveData,但是将根据我们是否激活PREMIUM_SKU来返回true或false:

Let's create a new LiveData which uses this purchases LiveData, but instead will return true or false based on whether we have the PREMIUM_SKU active:

public LiveData<Boolean> isSubscriptionActive = Transformations.map(purchases, purchases -> {
    boolean hasSubscription = false;
    for (Purchase purchase : purchases) {
        // TODO: Also check for the other SKU's if needed
        if (Constants.PREMIUM_SKU.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()) {
            // This purchase is purchased and acknowledged, it is currently active!
            hasSubscription = true;
        }
    }
    return hasSubscription;
});

在BillingClientLifecycle中添加此区块,如果购买清单发生更改,它将发出true或false值

  1. 观察

与往常一样,在要接收更新的活动中观察此LiveData:

As usual, observe this LiveData in the Activity in which you want to receive the update:

billingClientLifecycle.isSubscriptionActive.observe(this, hasSubscription -> {
    if (hasSubscription) {
        // User is subscribed!
        Toast.makeText(this, "User has subscription!", Toast.LENGTH_SHORT).show();
    } else {
        // User is a regular user!
    }
});

根据您的情况将其放在MainActivity中.它将观察订阅的更改,并在更改时触发两个功能之一.

Put this in the MainActivity in your case. It will observe for the subscription changes and trigger in one of the two functions when it changes.

*如果不需要实时数据,而是直接获取值的方法,则还可以使用billingClientLifecycle中的布尔值字段,并使用相同的检查在processPurchases()方法中正确更新此值如上所述.

* If the livedata is not wanted but rather a direct way of retrieving the value, you can also just use a boolean field inside the billingClientLifecycle and update this correctly at the processPurchases() method with the same check as above.

高级

对于更高级的用法,我们还可以使用购买对象的其他状态:

For a more advanced usage, we can also use the other states of the purchase object:

如果购买的状态为Purchase.PurchaseState.PENDING,则表示Google或用户仍然需要执行一些步骤来验证付款.基本上,这意味着计费API无法确定付款是否已完成(如果发生这种情况).例如,我们也可以通过显示一条消息来告知用户有关此状态的信息,以通知他.

In case the purchase has a state of Purchase.PurchaseState.PENDING, it means that the Google or the User still have some steps to do to verify the payment. Basically this means that the billing API is not sure whether the payment was fulfilled if this happens. We could inform the user about this state for example too by showing a message to fulfil his payment or so.

如果已付款但尚未确认购买,则表示BillingClientLifecycle中的确认步骤不成功.此外,在这种情况下,Google Play会自动将付款退还给用户.例如:对于每月订阅,确认期为3天,因此3天后,用户会收回资金并删除购买.

If a purchase is paid but not acknowledged yet, it means the acknowledge step in the BillingClientLifecycle was not successful. Additionally, if this is the case, Google Play will automatically refund the payment to the user. For example: for monthly subscriptions the acknowledgement period is 3 days, so after 3 days the user gets the money back and the purchase is removed.

这篇关于Google Play计费API:如何了解用户已订阅?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆