anjlab / android-inapp-billing-v3

A lightweight implementation of Android In-app Billing Version 3
Other
2.19k stars 535 forks source link

Question: How do I verify canceled subscription? #398

Open haroldogtf opened 5 years ago

haroldogtf commented 5 years ago

I am using: bp.isSubscribed(ID) to verify my subscription. But when I cancel a subscription the method bp.isSubscribed(ID) still returning true. What should I do?

ghost commented 5 years ago

I was also facing the similar. This is what I did to get around it.

Invoke the method bp.loadOwnedPurchasesFromGoogle(); Then check the value of transactionDetails, if the TransactionDetails object return null it means, that the user did not subscribe or cancelled their subscription, otherwise they are still subscribed.

`

private void updateTextViews() {
    checkIfUserIsSusbcribed();
}

void checkIfUserIsSusbcribed(){
    boolean purchaseResult = bp.loadOwnedPurchasesFromGoogle();
    if(purchaseResult){
        TransactionDetails subscriptionTransactionDetails = bp.getSubscriptionTransactionDetails(SUBSCRIPTION_ID);
        if(subscriptionTransactionDetails!=null) {
            //User is still subscribed
            showToast("User is still subscribed");
        } else {
            //Not subscribed
            showToast("Not subscribed");
        }
    }
}

`

Also, point to note is that the TransactionDetails object will only return null after the period of the subscription has expired.

haroldogtf commented 5 years ago

Hi @KRIPT4 I still getting true using your code even when I cancel the subscription. There are something more to do?

wishie commented 5 years ago

I have noticed that bp.loadOwnedPurchasesFromGoogle() will return false, and therefore the rest of the code is not run..

I can't believe its so hard to get an accurate answer on if a users subscription is still valid or not.

ghost commented 5 years ago

@haroldogtf It worked for me. Beware of response times! The subscription is not canceled at the moment, it may take a few minutes, I have seen cases of 5 to 10 minutes until it disappears from Google Play.

wishie commented 5 years ago

Here is my logic, and I get strange results..

So in MainActivity.java, I check the subscription status via the following (I also use the same code block in the AppWidgetProvider.java so that it checks subscription status each time the widget gets refreshed -- every 1hr):

bp = new BillingProcessor(this, "LICENSEKEYHERE", this);
        bp.initialize();

        boolean purchaseResult = bp.loadOwnedPurchasesFromGoogle();
        if(purchaseResult) {
            TransactionDetails subscriptionTransactionDetails = bp.getSubscriptionTransactionDetails("widget_subscription");
            if (subscriptionTransactionDetails != null) {
                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
                SharedPreferences.Editor editor = sp.edit();
                editor.putBoolean("widgetPurchased", true);
                editor.apply();
            }else{
                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
                SharedPreferences.Editor editor = sp.edit();
                editor.putBoolean("widgetPurchased", false);
                editor.apply();
            }
        }

The code in MainActivity.java to handle the purchase flow is:

boolean purchaseResult = bp.loadOwnedPurchasesFromGoogle();
                if(purchaseResult) {
                    TransactionDetails subscriptionTransactionDetails = bp.getSubscriptionTransactionDetails("widget_subscription");
                    if (subscriptionTransactionDetails != null) {
                        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
                        SharedPreferences.Editor editor = sp.edit();
                        editor.putBoolean("widgetPurchased", true);
                        editor.apply();
                        fragment = (Fragment) fragmentClass.newInstance();
                        getSupportFragmentManager().beginTransaction().replace(R.id.flContent, fragment, tag).commitAllowingStateLoss();

                    } else {
                        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
                        SharedPreferences.Editor editor = sp.edit();
                        editor.putBoolean("widgetPurchased", false);
                        editor.apply();
                        SweetAlertDialog sweetAlertDialog = new SweetAlertDialog(this, SweetAlertDialog.CUSTOM_IMAGE_TYPE)
                                .setTitleText("Subscribe")
                                .setContentText("The BIG Ferry Planner widget is a premium feature. Please subscribe to use it.")
                                .setCustomImage(R.drawable.ic_app)
                                .setConfirmText("Ok, got it")
                                .setConfirmClickListener(new SweetAlertDialog.OnSweetClickListener() {
                                    @Override
                                    public void onClick(SweetAlertDialog sDialog) {
                                        sDialog.dismissWithAnimation();
                                        //bp.purchase(MainActivity.this, "android.test.purchased");
                                        bp.subscribe(MainActivity.this, "widget_subscription");
                                    }
                                });
                        sweetAlertDialog.setCancelable(false);
                        sweetAlertDialog.show();
                    }
                }

So, what am I doing wrong? Why do I get strange and unpredictable results?

ghost commented 5 years ago

This is an example to remove the adView. I have it working properly in 20 Apps. Adapt it to your application, it should work.

`

package com.test;

import .;

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {

////////////////////////////////////////////////////////////////////////////////////////////////

// SAMPLE APP CONSTANTS
private static final String ACTIVITY_NUMBER = "activity_num";
private static final String LOG_TAG = "IN-APP-TEST";

// PRODUCT & SUBSCRIPTION IDS
//private static final String PRODUCT_ID = "buyproduct";
private static final String SUBSCRIPTION_ID = "subspro";
private static final String LICENSE_KEY = "****************"; // PUT YOUR MERCHANT KEY HERE;
// put your Google merchant id here (as stated in public profile of your Payments Merchant Center)
// if filled library will provide protection against Freedom alike Play Market simulators
private static final String MERCHANT_ID=null;

Boolean subsProOK = false;

private BillingProcessor bp;
private boolean readyToPurchase = false;

AdView mAdView;

////////////////////////////////////////////////////////////////////////////////////////////////

@Override
protected void onCreate(Bundle savedInstanceState) {

    ////////////////////////////////////////////////////////////////////////////////////////////

    if(!BillingProcessor.isIabServiceAvailable(this)) {
        Toast.makeText(this, "In-app billing service is not available, update Android Market / Google Play Store to version> = v3.9.16", Toast.LENGTH_LONG).show();
    }

    bp = new BillingProcessor(this, LICENSE_KEY, MERCHANT_ID, new BillingProcessor.IBillingHandler() {
        @Override
        public void onProductPurchased(@NonNull String productId, @Nullable TransactionDetails details) {
            //showToast("onProductPurchased: " + productId);
            updateTextViews();
        }
        @Override
        public void onBillingError(int errorCode, @Nullable Throwable error) {
            //showToast("onBillingError: " + Integer.toString(errorCode));
        }
        @Override
        public void onBillingInitialized() {
            //showToast("onBillingInitialized");
            readyToPurchase = true;
            updateTextViews();
        }
        @Override
        public void onPurchaseHistoryRestored() {
            //showToast("onPurchaseHistoryRestored");
            for(String sku : bp.listOwnedProducts())
                Log.d(LOG_TAG, "Owned Managed Product: " + sku);
            for(String sku : bp.listOwnedSubscriptions())
                Log.d(LOG_TAG, "Owned Subscription: " + sku);
            updateTextViews();
        }
    });

    ////////////////////////////////////////////////////////////////////////////////////////////

    mAdView = view.findViewById(R.id.adView);

    if (!subsProOK){
        mAdView.setVisibility(View.VISIBLE);
        AdRequest adRequest = new AdRequest.Builder().build();
        mAdView.loadAd(adRequest);
    } else {
        mAdView.setVisibility(View.GONE);
    }

}

////////////////////////////////////////////////////////////////////////////////////////////////

private void updateTextViews() {
    checkIfUserIsSusbcribed();
}

void checkIfUserIsSusbcribed(){
    boolean purchaseResult = bp.loadOwnedPurchasesFromGoogle();
    if(purchaseResult){
        TransactionDetails subscriptionTransactionDetails = bp.getSubscriptionTransactionDetails(SUBSCRIPTION_ID);
        if(subscriptionTransactionDetails!=null) {
            //User is still subscribed
            //showToast("User is still subscribed");
            subsProOK = true;
        } else {
            //Not subscribed
            //showToast("Not subscribed");
            subsProOK = false;
        }
    }
}

@Override
public void onDestroy() {
    if (bp != null) {
        bp.release();
    }
    super.onDestroy();
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (!bp.handleActivityResult(requestCode, resultCode, data))
        super.onActivityResult(requestCode, resultCode, data);
}

@Override
protected void onResume() {
    super.onResume();
    updateTextViews();
}

}

`

wishie commented 5 years ago

I think I have tracked down the issue...

I use the following in MainActivity.java:

boolean purchaseResult = bp.loadOwnedPurchasesFromGoogle();

                if(purchaseResult){
                    TransactionDetails subscriptionTransactionDetails = bp.getSubscriptionTransactionDetails("widget_subscription");
                    if(subscriptionTransactionDetails!=null) {
                        //User is still subscribed
                        Log.d("BILLING ", "Subscription is valid");
                        fragment = (Fragment) fragmentClass.newInstance();
                        getSupportFragmentManager().beginTransaction().replace(R.id.flContent, fragment, tag).commitAllowingStateLoss();
                    } else {
                        //Not subscribed
                        Log.d("BILLING ", "Subscription is NOT valid");
                        SweetAlertDialog sweetAlertDialog = new SweetAlertDialog(this, SweetAlertDialog.CUSTOM_IMAGE_TYPE)
                                .setTitleText("Subscribe")
                                .setContentText("The BIG Ferry Planner widget is a premium feature. Please subscribe to use it.")
                                .setCustomImage(R.drawable.ic_app)
                                .setConfirmText("Ok, got it")
                                .setConfirmClickListener(new SweetAlertDialog.OnSweetClickListener() {
                                    @Override
                                    public void onClick(SweetAlertDialog sDialog) {
                                        sDialog.dismissWithAnimation();
                                        Log.d("BILLING", "Starting Widget Purchase Flow");
                                        bp.subscribe(MainActivity.this, "widget_subscription");
                                    }
                                });
                        sweetAlertDialog.setCancelable(false);
                        sweetAlertDialog.show();
                    }
                }else{
                    Log.d("BILLING", "loadOwnedPurchasesFromGoogle returned false");
                }

But, if I use similar code in AppWidgetProvider.java, loadOwnedPurchasesFromGoogle() always returns FALSE.

So perhaps it's because its being run in an AppWidget, not in the Application itself..

ghost commented 5 years ago

It is possible, although I execute it in every activity or fragment.

wishie commented 5 years ago

Looking at what loadOwnedPurchasesFromGoogle actually calls (not at a PC now so can't remember function names) it ultimately uses getContext().getPackageName() which may very well not return the name of the parent app.

This would cause it not to know which app to check purchases for, therefore failing (returning false).

All speculation, but I think I'm on the right track.

wishie commented 5 years ago

Ok, finally at a PC.. so, in the library

loadOwnedPurchasesFromGoogle()

does the following:

public boolean loadOwnedPurchasesFromGoogle()
    {
        return loadPurchasesByType(Constants.PRODUCT_TYPE_MANAGED, cachedProducts) &&
               loadPurchasesByType(Constants.PRODUCT_TYPE_SUBSCRIPTION, cachedSubscriptions);
    }

Inside loadPurchasesByType() you will find the following:

Bundle bundle = billingService.getPurchases(Constants.GOOGLE_API_VERSION,
                                                        contextPackageName, type, null);

And in BillingProcessor you can see the following:

private BillingProcessor(Context context, String licenseKey, String merchantId, IBillingHandler handler,
                             boolean bindImmediately)
    {
        super(context.getApplicationContext());
        signatureBase64 = licenseKey;
        eventHandler = handler;
        contextPackageName = getContext().getPackageName();
        cachedProducts = new BillingCache(getContext(), MANAGED_PRODUCTS_CACHE_KEY);
        cachedSubscriptions = new BillingCache(getContext(), SUBSCRIPTIONS_CACHE_KEY);
        developerMerchantId = merchantId;
        if (bindImmediately)
        {
            bindPlayServices();
        }
    }

So, contextPackageName = getContext().getPackageName(); is obviously used to define the package name (app name) and I assume used in the call to google as "show me all the purchases that belong to 'appName' please".

Now, I speculate that getContext().getPackageName(); is not returning the name of my main package/app, as Im calling it from AppWidgetProvider.

LunevNF commented 4 years ago

This methods not works. Today I was unsubscribe (cancel my subscription), but this lib returns purchDetails.purchaseInfo.purchaseData.autoRenewing = true and returns List(BillingHistoryRecord) with size > 0. Google play showing, that user have no subscription (status - canceled).

So, all users can cancel subscription and use app free.

PS: bp.loadOwnedPurchasesFromGoogle() used every app starting, before checking purchases.

Sorry for my ugly russian english!

wishie commented 4 years ago

Once the user cancels, they have until the end of the current billing cycle to use the widget. After that, it will show as cancelled.

jarvanh commented 4 years ago

This methods not work to me too.

Like @nikitoSha said.

froozesocial commented 4 years ago

Does anyone have a solution?