Google play billing API:如何理解用户订阅? [英] Google play billing API: How to understand the user is subscribed?

本文介绍了Google play billing 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 类中使用共享首选项(而不是 Toast),并从 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 为真,则表示用户当前有一个活动订阅.这意味着如果用户取消订阅但在结束期间仍然支付,该值仍然为真.仅仅是因为用户在计费期到期之前仍然有权访问内容.

* 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,但会根据 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. 观察它

像往常一样,在你想要接收更新的 Activity 中观察这个 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 billing API:如何理解用户订阅?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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