j3k0 / cordova-plugin-purchase

In-App Purchase for Cordova on iOS, Android and Windows
https://purchase.cordova.fovea.cc
1.3k stars 537 forks source link

Whats the trigger for subscriptions (auto-renew and non-auto-renew)? #212

Closed rwillett closed 7 years ago

rwillett commented 9 years ago

We've now set up a number of sandbox products, some are one off purchases, some are auto-renew subscriptions and one is a non-renewable weekly subscription.

We believe we have set up all the correct queries in a simple to use test environment. The function below iterates through an array of products and sets up the all the call back routines. We have a validating server setup as well and can see things coming in. The validating server does not actually validate and simply returns the correct JSON for its valid so it always returns valid.

We can certainly see lots of information and state changes and we think that the code works.

Most of the code below is to simply track the purchases through the various states with the exception of finishing a product order.

    function InitialiseStore()
    {
        ConsoleLog("InitializeStore started");

        store.verbosity = store.INFO;

        // Enable remote receipt validation
        store.validator = "http://XXX.XXX.XXX:YYYY/check-purchase";

        for (var i = 0 ; i < products.length ; i++)
        {
            // Clear out the store information as we DO have a store to use.
            products[i].store_info = {};

            ConsoleLog("Registering " +  products[i].id + " " + products[i].alias + " " + products[i].type);
            store.register({
                id:    products[i].id ,
                alias: products[i].alias ,
                type:  products[i].type
            });
        }

        store.ready(function() {
            ConsoleLog("*** STORE READY ***");

            for (var i = 0 ; i < products.length ; i++)
            {
                var p = store.get(products[i].id);

                products[i].store_info = p;

                store.when(products[i].store_info.id).loaded(function(product) {
                    StoreProductPrint("Loaded: " , product);
                }).updated(function(product) {
                    // var info = store.get(product.id);
                    StoreProductPrint("Store: Updated: " , product);
                }).error(function(product) {
                    StoreProductPrint("Store: Error: " , product);
                }).approved(function(product) {
                    StoreProductPrint("Store: Approved: " , product);

                    product.verify().success(function (product , purchaseData) {
                        StoreProductPrint("Verified - Success:" , product);
                        product.finish();
                    }).error(function(err) {
                        ConsoleLog("Verified - Error: " + JSON.stringify(err));
                    }).done(function(product) {
                        StoreProductPrint("Verified - Done:" , product);
                    });
                }).owned(function(product) {
                    StoreProductPrint("Store: Owned: " , product);
                }).cancelled(function(product) {
                    StoreProductPrint("Store: Cancelled: " , product);
                }).refunded(function(product) {
                    StoreProductPrint("Store: Refunded: " , product);
                }).registered(function(product) {
                    StoreProductPrint("Store: Registered: " , product);
                }).valid(function(product) {
                    StoreProductPrint("Store: Valid: " , product);
                }).invalid(function(product) {
                    StoreProductPrint("Store: Invalid: " , product);
                }).refunded(function(product) {
                    StoreProductPrint("Store: Refunded: " , product);
                }).requested(function(product) {
                    StoreProductPrint("Store: Requested: " , product);
                }).initiated(function(product) {
                    StoreProductPrint("Store: Initiated: " , product);
                }).finished(function(product) {
                    StoreProductPrint("Store: Finished: " , product);
                }).verified(function(product) {
                    StoreProductPrint("Store: Verified:" , product);
                }).unverified(function(product) {
                    StoreProductPrint("Store: Unverified: " , product);
                }).expired(function(product) {
                    StoreProductPrint("Store: Expired: " , product);
                });
            }
        });

        // After we've done our setup, we tell the store to do
        // it's first refresh. Nothing will happen if we do not call store.refresh()
    store.refresh();
    }

We can buy a product using a sandbox account and we can see it working OK. Our issue is what triggers the expiration of either a non-renewable or a renewable subscription? Our expectation was that the code segment

.expired(function(product) {
                    StoreProductPrint("Store: Expired: " , product);
                });

in the above function would be called by something (we know not what). But it seems to be that the expiration callback never gets called by anything. We never see it in the logs anyway.

Have we misunderstood the state model?

Or do we implement expiration of subscriptions by calling the validation server and getting stuff back from there, in which case why do we have the .expired call back defined? if so is there a function call for calling the validation server doing this? I know we can write our own but wondered if there is one.

Do we simply record the fact that the subscription is verified and we keep that logic and value in our client and act on that and never call anything else, so that we know in a months time that the subscription is running out and so stop functionality being available?

Should we be calling

    store.refresh();

and then processing the output or is that too expensive?

Are we missing something else?

Any help would be appreciated so we can use best practises here.

Thanks,

Rob

j3k0 commented 9 years ago
rwillett commented 9 years ago

Thanks for the reply.

We've added the PURCHASE_EXPIRED code to the server and that seems to have partially worked, though the client behaviour is still somewhat odd.

Once we buy a subscription (a weekly one), the owned flag gets set to true. Once we expire the subscription, the owned flag gets set to false. We manually expire a subscription on the validation server by changing the return code on the server and then manually refreshing the data by calling

store.refresh();

The validation server gets called, it returns the expired error code and owned gets changed to false on the client. We can see this happening in the client interface, including canPurchase being changed to false as we get prompted to buy.

If we then buy another subscription, we get prompted for our password, we get prompted for confirmation and then an alert pops up saying we have already brought this subscription. Searching through the issues logs, this seems to be a common problem. Looking through the doc, as far as we can see, all we need the status of owned == false to buy a subscription again.

Any ideas what triggers the already purchased message?

We've created new users, we've expired as much as we can, we've deleted products and generally become more and more frustrated.

All suggestions welcomed,

Thanks,

Rob

j3k0 commented 9 years ago

If you server replies "expired" but it's not really expired on Apple's server, StoreKit SDK won't allow the user to re-purchase a subscription to which the user is already subscribed.

rwillett commented 9 years ago

Thanks, this is all related to #215. As I say there, we will actually build just the sample app because this is driving us up the wall now.

Rob.

rwillett commented 9 years ago

I'm closing this as I think its now resolved.

rwillett commented 9 years ago

Mmm...I received an e-mail from Stefan Yanku asking how we resolved this issue.

  1. Its not clear what Stefan is asking as there's a lot of stuff in this e-mail trail.

If the question is about the dialogue box that opens when you have brought a 'simple' non-renewing subscription, then we have not resolved it. As I understand it this dialogue box saying you have already brought the subscription is actually the right behaviour. If it is, then the message box is very, very confusing. We cannot find any other app that shows the dialogue box to check, so we are still investigating.

  1. If the question was about the server implementation code, then yes we resolved it. We generate nonce's and track everything in a database in Perl. There are some tricky bits in it, such as distinguishing between a new transaction to be logged and an existing transaction to be verified. Big difference. There's around 200-250 lines of Perl to handle it all.
  2. If its not any of the above I have no idea :)

Rob

yankustefan commented 9 years ago

Hi Rob,

Thank you very much for your response! I believe I am finally able to wrap my head around your issue, after familiarizing myself a bit more with this plugin's purchase workflow.

Yes, there was a lot going on in this thread, so I thought I take my question back to avoid confusion. Also because I have not yet begun to work on the server side part to verify the subscriptions. So it was probably a little premature to hit the comment button.

Thanks anyway!

rwillett commented 9 years ago

I'd be interested in what conclusions you draw as the process flow in all of this is tricky.

Areas we are still worried about:

  1. I'm still not convinced by the dialogue box that comes up with non-auto renewing transactions. I'd love to find another app that implements non auto renewing transactions and see what they show.
  2. The verification stage is tricky as you have to work out what new transactions are vs verifying existing ones. We look for the existence of the transaction.id value (from memory) to handle this. I would have expected to see something else, perhaps we missed it.
  3. The AppStoreReceipts are getting bigger and bigger which is a worry. Do they ever stop growing? All consumable purchases seem to be in there. We haven't played with renewable subscriptions yet as we want to understand what we're doing.
  4. The inability of the Sandboxed environment to handle more than 4-5 renewable subscriptions.
  5. The general utter, utter awfulness of the sandbox testing regime. I have managed and run very large projects and if I had put the Apple sandboxed environment into our testing regime, I'd have been laughed at by the client as not fit for purpose.
  6. Android Purchases - We haven't even started on that as we want to get iOS working and stable.
  7. The fact that non-auto renewable subscriptions don;t have an expiry date and there does not appear to be anyway to tell Apple. This moves a load of code onto us which is a little surprising.

All in all its a hotchpotch of issues and problems and little (or big) gotcha's.

I'm sure there are loads more issues but these are the first ones that come to mind.

Rob

Pigsnuck commented 8 years ago

Hi Rob,

I have come to the same conclusion as you, that the expiry event only occurs if the server sends the correct response.

But my question is this: given that the receipt contains a squillion transactions for multiple products, if one product is VALID and another is EXPIRED, how does the server know which response to send?

Craig

rwillett commented 8 years ago

I'm afraid we abandoned using any in-app purchases or indeed outright purchases and made our app free.

We were coming to the conclusion that being free was a better idea anyway and we always had a two prong business model.

We looked at the costs of implementing any sort of purchasing whatsoever, given that Apple takes 30%, the fact that people demand gold plated support even for a $1 app, the sheer hassle of getting the application logic right (and we never did get it right), the crap sandbox environment and the fact that whilst we we might make some money it simply wasn't enough to justify or even cover all of the above.

So we simply said "no" and moved to a free client model.

It's been nine months since we looked at this and our code base has long been archived so I can't really answer the question as we have moved on so much since that time.

Apologies.

Rob

Pigsnuck commented 8 years ago

Well, thanks for the response. I am slowly unravelling the in-app purchase implementation for cordova. I implemented in-app purchases natively for iOS and Android in the past, and even though it's a bitch using this plugin, it's still much easier than doing a native implementation. The annoyances are almost all from the App Store / Google Play rather than the plugin. The plugin saved me a lot of time.

But that said, I can understand why you gave up. ;)