【问题标题】:Google play billing API: How to understand the user is subscribed?Google play billing API:如何了解用户是否订阅?
【发布时间】:2020-10-11 17:34:34
【问题描述】:

我想了解用户是否从 MainActivity 主动订阅了 Basic/Premium 内容。有一个BillingClientLifecycle 类启动订阅过程。据我了解,queryPurchses 应该显示用户是否有活动订阅。但显然它显示(通过我放在那里显示订阅状态的 Toasts)即使用户实际上没有订阅,用户也已订阅。

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 如下:

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 类或任何其他需要在启动应用程序时通知订阅状态的类中检索订阅状态。虽然我不喜欢使用共享偏好并直接调用订阅信息。

【问题讨论】:

    标签: android android-studio google-play-developer-api in-app-subscription play-billing-library


    【解决方案1】:

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

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


    采购清单

    我们先解释一下什么这里的购买列表在计费 API 中的确切含义:

    1. 这是用户购买应用内商品或订阅的所有列表。
    2. 这些购买必须由应用或后端确认(建议通过后端,但两者都可以)
    3. 此购买清单包括仍待处理的付款以及尚未确认的付款。

    看到正在执行的确认步骤,我假设付款确认已成功完成。

    第 3 点是它未检测到实际订阅状态的原因,因为未检查购买状态。


    检查订阅状态

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

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

    • getSku() // 验证产品是我们想要的
    • getPurchaseState() // 获取实际购买状态
    • isAcknowledged() // 知道是否支付成功,如果没有则说明支付尚未成功

    为了检查购买当前是否已支付并且对于 PREMIUM sku 有效:

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

    如果我们想检查任何订阅是否处于活动状态,我们会检查其他 sku 是否相同(循环通过 sku 和购买)

    * 请注意,现在如果isPremiumActive 为真,则表示用户当前 有一个活动订阅。这意味着如果用户取消了他的订阅但仍然支付到结束时间,这个值仍然是真实的。仅仅是因为用户在计费期到期之前仍然有权访问内容。

    * 如果订阅期真的结束(取消或过期),计费客户端将不再退回购买。


    观察当前状态

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

    1. 创建 LiveData

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

    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:

    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 中。它会观察订阅的变化并在两个函数之一发生变化时触发。

    * 如果不需要 livedata 而是直接检索值的方法,您也可以在 billingClientLifecycle 中使用布尔字段,并在 processPurchases() 方法中使用相同的检查正确更新它如上所述。


    高级

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

    如果购买的状态为Purchase.PurchaseState.PENDING,则意味着谷歌或用户仍有一些步骤可以验证付款。基本上这意味着,如果发生这种情况,计费 API 不确定付款是否已完成。例如,我们也可以通过显示一条消息以完成他的付款等来告知用户此状态。

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

    【讨论】:

    • 谢谢亚历克斯。我得到 NPE isSubscriptionActive' on a null object reference
    • 没问题!该 NPE 应该是因为您的 billingClientLifecyclenull 然后在调用它时。请在初始化billingClientLifecycle 后调用观察者。或者以其他方式:您是否也可以在创建 MainActivity 的位置以及将 MainActivity 与 billingClient 连接的位置共享 MainActivity 部分?
    • 我添加了 MainActivity
    • 太好了,谢谢!将billingClientLifecycle.isSubscriptionActive.observe 行移动到getLifecycle().addObserver(billingClientLifecycle); 下方的行。那应该可以解决它
    • 显示吐司。但总是因为用户没有任何订阅,即使用户最近购买了订阅。
    【解决方案2】:

    我正在使用这个库进行购买,它可能对你有帮助。

    https://github.com/anjlab/android-inapp-billing-v3

    接口BillingProcessor.IBillingHandler 在您的主要活动中实现

    private lateinit var mBillingProcessor: BillingProcessor
    val PRODUCT_ID = "remove_ads"//original, set as you want
    //val PRODUCT_ID = "android.test.purchased"//testing for purchase 
    //val PRODUCT_ID = "android.test.canceled"//testing for cancel purchase
    

    onCreate()方法中

    mBillingProcessor = BillingProcessor(this, "your_license_key", this)
    mBillingProcessor.initialize()
    //Here after initialization you can check subscription by
    if(mBillingProcessor.isSubscribed(PRODUCT_ID)){
       //user has Subscribed
    }else{
      //user has not Subscribed
    }
    

    当用户点击订阅时

    mBillingProcessor.subscribe(this, PRODUCT_ID)
    

    在ActivityResult上实现这个方法

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (!mBillingProcessor.handleActivityResult(requestCode, resultCode, data)) {
            super.onActivityResult(requestCode, resultCode, data)
        }
        super.onActivityResult(requestCode, resultCode, data)
    }
    

    重写这个库的方法

    override fun onProductPurchased(productId: String, details: TransactionDetails?) {
            if (mBillingProcessor.isPurchased(PRODUCT_ID).toString() == "true") {
               //here when user purchased successfully
            }
    }
    

    此方法返回布尔变量

    【讨论】:

    • 谢谢。它只是使用离线数据库吗?还是后端服务器?
    • 其实它使用com.android.billingclient:billing 库,它只是play billing 库的一个封装,它没有任何后端服务器是使用google play billing 库
    猜你喜欢
    • 1970-01-01
    • 2016-10-04
    • 1970-01-01
    • 2013-03-09
    • 1970-01-01
    • 2013-01-21
    • 1970-01-01
    • 2019-12-01
    • 1970-01-01
    相关资源
    最近更新 更多