Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google play billing API: How to understand the user is subscribed?

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();
            }
        }
    }

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);
        }
    });
}

}

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.

like image 842
Namikaze Minato Avatar asked Jun 21 '20 21:06

Namikaze Minato


People also ask

What does subscriber mean on Google Play?

You can subscribe to these apps with Google Play. When you subscribe: You'll automatically be charged each renewal period depending on your subscription terms, for example: weekly, or annually. If you accept a $0/month trial offer, you'll get an email when your trial is about to end.

How do I check app subscriptions on Android?

To view and manage your Android subscriptions, open the Google Play Store app on your device, then tap the menu button (the three horizontal lines in the top left), then choose Subscriptions. You'll see a list of all the subscriptions, such as Google Play Music, handled through Google.


2 Answers

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

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:


Purchases list

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

  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.

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


Checking the subscribed state

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() // 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

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()

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)

* 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.

* In case the subscription period is really over (cancelled or expired), the billing client will not return the purchase anymore.


Observing the current status

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. Creating the LiveData

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;
});

Add this block in the BillingClientLifecycle, it will emit the value true or false if the purchases list changes

  1. Observing it

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!
    }
});

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.

* 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.


Advanced

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

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.

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.

like image 190
Alex Avatar answered Oct 13 '22 01:10

Alex


I am using this library for purchases, It may be helpful for you.

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

interface BillingProcessor.IBillingHandler to implement in your main 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

in onCreate() method

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
}

when user click for subscription

mBillingProcessor.subscribe(this, PRODUCT_ID)

implement this method onActivityResult

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 method of this library

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

this method return you boolean variable

like image 28
Abdur Rehman Avatar answered Oct 13 '22 00:10

Abdur Rehman