Google play billing API: 如何理解用户是否订阅?
Google play billing API: How to understand the user is subscribed?
我想从 MainActivity 中查明用户是否主动订阅 Basic/Premium 内容。有一个 BillingClientLifecycle
class 发起订阅过程。据我了解,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();
}
}
}
我在这里做错了什么?我想根据订阅状态更新主activity。 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
class 中使用共享首选项(而不是 Toasts)并从 MainActivity class 或任何其他 classes 需要在启动应用程序时通知订阅状态。虽然我不喜欢使用共享首选项并直接调用订阅信息。
计费过程的实现看起来不错,但缺少一个检查以确定当前订阅是否真的有效。
可以使用 LiveData 对象进行观察。这样我们就不需要 SharedPreferences 等来保持状态。我将在下面的观察部分介绍这一点。详细回答:
购买清单
先解释一下什么这里的购买清单在账单中的确切含义API:
- 这是 所有 用户购买的应用内商品或订阅的列表。
- 这些购买必须由应用程序或后端确认(推荐通过后端,但两者都可以)
- 此购买清单包括 仍在等待 的付款以及未确认的付款还没有。
看到正在执行的确认步骤,我假设付款确认已成功完成。
第 3 点是它不检测实际订阅状态的原因,因为未检查购买状态。
检查订阅状态
queryPurchases()
调用 return 用户为请求的产品付款。我们收到的数组可以有多个项目(大多数是每个应用内项目或订阅一个)。我们需要全部检查一下。
每次购买都有更多的数据。以下是我们检查状态所需的方法:
getSku()
// 验证产品是我们想要的
getPurchaseState()
// 获取实际购买状态
isAcknowledged()
// 支付是否被确认,如果没有,说明还没有支付成功
为了检查当前购买是否已付款并且 激活了 PREMIUM sku:
boolean isPremiumActive = Constants.PREMIUM_SKU.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()
如果我们想检查是否有任何订阅处于活动状态,我们会检查其他 sku 是否相同(遍历 sku 和购买)
* 请注意,现在如果 isPremiumActive
为真,则表示用户 当前 具有有效订阅。这意味着如果用户取消了他的订阅但仍然支付到结束期间,这个值仍然是真实的。仅仅是因为在计费周期到期之前,用户仍然有权访问内容。
*如果订阅期真的结束(取消或过期),计费客户端将不再return购买。
观察当前状态
现在我们知道如何验证购买,我们可以使用 LiveData 轻松读取此状态,以便我们随时访问它。在示例中,我们已经有 te LiveData purchases
,这个包含所有购买并在 queryPurchases()
调用后填充。
- 创建 LiveData
让我们创建一个新的 LiveData,它使用这个 purchases
LiveData,但是 return true 或 false 取决于我们是否有 PREMIUM_SKU 活动:
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
- 正在观察
像往常一样,在您想要接收更新的 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
中。它将观察订阅变化并在变化时触发两个函数之一。
* 如果不需要实时数据,而是直接检索值的方式,您也可以只使用 billingClientLifecycle
中的布尔字段,并在 processPurchases()
方法与上面相同的检查。
高级
更高级的用法,我们还可以使用购买对象的其他状态:
如果购买状态为 Purchase.PurchaseState.PENDING
,则表示 Google 或用户仍需执行一些步骤来验证付款。基本上这意味着如果发生这种情况,账单 API 不确定付款是否已完成。例如,我们也可以通过显示完成付款的消息来告知用户此状态。
如果购买已付款但尚未确认,则表示 BillingClientLifecycle
中的确认步骤不成功。此外,如果是这种情况,Google Play 会自动将付款退还给用户。例如:对于按月订阅,确认期为 3 天,因此 3 天后用户可以取回款项并取消购买。
我正在使用这个图书馆进行购买,它可能对你有帮助。
https://github.com/anjlab/android-inapp-billing-v3
接口 BillingProcessor.IBillingHandler
在您的主 activity
中实现
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
}
}
这个方法return你布尔变量
我想从 MainActivity 中查明用户是否主动订阅 Basic/Premium 内容。有一个 BillingClientLifecycle
class 发起订阅过程。据我了解,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();
}
}
}
我在这里做错了什么?我想根据订阅状态更新主activity。 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
class 中使用共享首选项(而不是 Toasts)并从 MainActivity class 或任何其他 classes 需要在启动应用程序时通知订阅状态。虽然我不喜欢使用共享首选项并直接调用订阅信息。
计费过程的实现看起来不错,但缺少一个检查以确定当前订阅是否真的有效。
可以使用 LiveData 对象进行观察。这样我们就不需要 SharedPreferences 等来保持状态。我将在下面的观察部分介绍这一点。详细回答:
购买清单
先解释一下什么这里的购买清单在账单中的确切含义API:
- 这是 所有 用户购买的应用内商品或订阅的列表。
- 这些购买必须由应用程序或后端确认(推荐通过后端,但两者都可以)
- 此购买清单包括 仍在等待 的付款以及未确认的付款还没有。
看到正在执行的确认步骤,我假设付款确认已成功完成。
第 3 点是它不检测实际订阅状态的原因,因为未检查购买状态。
检查订阅状态
queryPurchases()
调用 return 用户为请求的产品付款。我们收到的数组可以有多个项目(大多数是每个应用内项目或订阅一个)。我们需要全部检查一下。
每次购买都有更多的数据。以下是我们检查状态所需的方法:
getSku()
// 验证产品是我们想要的getPurchaseState()
// 获取实际购买状态isAcknowledged()
// 支付是否被确认,如果没有,说明还没有支付成功
为了检查当前购买是否已付款并且 激活了 PREMIUM sku:
boolean isPremiumActive = Constants.PREMIUM_SKU.equals(purchase.getSku()) && purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()
如果我们想检查是否有任何订阅处于活动状态,我们会检查其他 sku 是否相同(遍历 sku 和购买)
* 请注意,现在如果 isPremiumActive
为真,则表示用户 当前 具有有效订阅。这意味着如果用户取消了他的订阅但仍然支付到结束期间,这个值仍然是真实的。仅仅是因为在计费周期到期之前,用户仍然有权访问内容。
*如果订阅期真的结束(取消或过期),计费客户端将不再return购买。
观察当前状态
现在我们知道如何验证购买,我们可以使用 LiveData 轻松读取此状态,以便我们随时访问它。在示例中,我们已经有 te LiveData purchases
,这个包含所有购买并在 queryPurchases()
调用后填充。
- 创建 LiveData
让我们创建一个新的 LiveData,它使用这个 purchases
LiveData,但是 return true 或 false 取决于我们是否有 PREMIUM_SKU 活动:
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
- 正在观察
像往常一样,在您想要接收更新的 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
中。它将观察订阅变化并在变化时触发两个函数之一。
* 如果不需要实时数据,而是直接检索值的方式,您也可以只使用 billingClientLifecycle
中的布尔字段,并在 processPurchases()
方法与上面相同的检查。
高级
更高级的用法,我们还可以使用购买对象的其他状态:
如果购买状态为 Purchase.PurchaseState.PENDING
,则表示 Google 或用户仍需执行一些步骤来验证付款。基本上这意味着如果发生这种情况,账单 API 不确定付款是否已完成。例如,我们也可以通过显示完成付款的消息来告知用户此状态。
如果购买已付款但尚未确认,则表示 BillingClientLifecycle
中的确认步骤不成功。此外,如果是这种情况,Google Play 会自动将付款退还给用户。例如:对于按月订阅,确认期为 3 天,因此 3 天后用户可以取回款项并取消购买。
我正在使用这个图书馆进行购买,它可能对你有帮助。
https://github.com/anjlab/android-inapp-billing-v3
接口 BillingProcessor.IBillingHandler
在您的主 activity
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
}
}
这个方法return你布尔变量