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

[IOS] Validator called 100's of times on refresh #808

Closed selected-pixel-jameson closed 5 years ago

selected-pixel-jameson commented 5 years ago

system info

macOS Mojave 10.14.3
Device: iPhone iOS 12
Plugin Version: 7.4.3

I'm setting up the validator with a custom callback like so. this.store.validator = (product,callback) => this.verifyReceiptCallback(product, callback);

The verifyReceiptCallback function looks like:

      verifyReceiptCallback = function(product, callback){
        console.log("-----------------------------")
        console.log("VERIFY RECEIPT")
        console.log("-----------------------------")

        this.verifyReceipt(product).subscribe(
          res => {
            if(res.ok){
              console.log("RECEIPT VERIFIED SUCCESSFULLY");
              callback(true, res.data);
            }else{
              if(res.expired){
                console.log("RECEIPT EXPIRED");
                callback(false,{code:this.store.PURCHASE_EXPIRED, error:{message:"Purchase Expired"}})
              }else{
                console.log("UNABLE TO VALIDATE");
                callback(false, "Unable to validate purchase.");
              }
            }
          },
          err => {
            console.log("UNABLE TO VALIDATE: ", JSON.stringify(err));
            callback(false, "Unable to validate purchase.");
          }
        )

      };

      verifyReceipt(product){
             return this.api.post("iap-verify-receipt/", product, this.api.AuthorizationHeaders)
             .map((response:Response) => <any>response.json())
             .catch(this.handleError); 
      }

When I register my products within the store and setup the listeners I make sure that I am only registering the listeners one time for each of the two products I have.

private configurePurchasing(productId) {
        console.log("-------------------------------------------------------------------");
        console.log('Starting Configurations');
        console.log("-------------------------------------------------------------------");

        if(!this.registeredProducts.includes(productId)){

          // Register Product
          console.log("-------------------------------------------------------------------");
          console.log('Registering Product ' + JSON.stringify(productId));
          console.log("-------------------------------------------------------------------");

          this.store.register({
              id: productId,
              alias: productId,
              type: this.store.PAID_SUBSCRIPTION
          });

          this.registeredProducts.push(productId);

          // Handlers
          this.store.when(productId).approved( (product: IAPProduct) => {
              console.log("-------------------------------------------------------------------");
              console.log("Approved");
              console.log("Product Status : ", JSON.stringify(product.state))
              console.log("-------------------------------------------------------------------");
              product.verify();
              console.log("-------------------------------------------------------------------");

          });

          this.store.when(productId).updated( (product: IAPProduct) => {
              console.log("-------------------------------------------------------------------");
              console.log('Updated: ');
              console.log("Product Status : ", JSON.stringify(product.state))
              console.log("-------------------------------------------------------------------");
              console.log("-------------------------------------------------------------------");
              if(product.state == 'owned'){
                this.events.publish('product:owned');
              }

              if(product.state == 'valid'){
                var foundIndex = this.products.findIndex(p => p.id == product.id);
                if(foundIndex == -1){
                  this.products.push(product);
                }else{
                  this.products[foundIndex] = product;
                }
              }

          });

          this.store.when(productId).cancelled( (product) => {
              console.log("-------------------------------------------------------------------");
              console.log("PURCHASE CANCELLED OR DISMISSED")
              console.log("-------------------------------------------------------------------");
              this.events.publish("purchase:cancelled");
          });

          this.store.when(productId).verified(function(product) {
              console.log("-------------------------------------------------------------------");
              console.log("Verified", product.id);
              console.log("-------------------------------------------------------------------");
              product.finish();
              //this.subscribe();
          });

          this.store.when(productId).unverified(function(product) {
              console.log("-------------------------------------------------------------------");
              //console.log("Unverified: " + JSON.stringify(product));
              console.log("-------------------------------------------------------------------");
          });

          // Errors
          this.store.when(productId).error( (error) => {
              this.events.publish("product:error");
          });

        }else{           
            console.log("PRODUCT ALREADY REGISTERED");
        }
}

After all the products are registered I call store.refresh() and it literally kicks off 100+ verification requests to my server.

I'm not sure what I have setup wrong, or maybe that's just how it works? But it continues to request verification on subscriptions that I am verify and they are coming back as expired.

Does the callback function need to use the await function so that these aren't all executed simultaneously?

This is within the sandbox and I'm using auto-renewing subscriptions. Based on that I understand that I probably have an abnormal amount of transactions because of the 5 minute expiration, but still it seems like it shouldn't need to hit that endpoint over and over again.

I've attached a log which is really just simple event logging that shows this. This is the out put of me requesting a 'Restore of Purchases' for the second time. The log itself is over 2000 lines of code, but makes it pretty apparent how often the events are being called. iap-logs.txt

selected-pixel-jameson commented 5 years ago

To further elaborate on this. I logged in with a different test App Store account and this no longer happens. It seems as if there has to be a way to not run through every single transaction that the user has in the receipt. Once again I understand this won't be nearly as big of a problem with production subscriptions, but even with a user who has a monthly subscription by the end of the first year this would make 12 requests to the server if they got a new device and 'restored the purchase' on it.

j3k0 commented 5 years ago

It's the current behavior. I implemented a fix in the testing branch that'll I integrate into the master branch as soon as I find some time.

Some background: in the old days, Apple provided 1 receipt per transaction, so had to validate the individual transactions. Their API is still transaction based, and iOS will trigger the purchase event for every transactions made since the edge of times when hitting "Restore Purchases" (regardless if they are long-expired subscription renewals). The plugin should be doing some filtering instead of blindly bubbling everything up.

lalmanzar commented 4 years ago

@j3k0 Any update on when this will hit release?

ipehimanshu commented 2 years ago

i see same issue for android,

validator called many times, even product.finish called, any one fix that issue then please help .

Thanks in advance.