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

[ANDROID] product.owned always returns false for subscription #731

Closed Nostrus closed 5 years ago

Nostrus commented 6 years ago

System info

Windows 10
Node version: v8.12.0
Cordova version: 8.0.0
Android 7.0
Plugin Version: 7.2.0 
Using Ionic with package: "@ionic-native/in-app-purchase-2": "^4.15.0"

Expected behavior

Using this example product.owned should return true when subscription is active, used in the callback of this.iap.when(PRODUCT_ID).updated.

Observed behavior

product.owned returns false for active subscription. (Not sure if and when I should call product.finish() at any point for subscriptions to change the product state, but example doesn't say I should.)

Steps to reproduce

  1. Purchase test subscription with test card
  2. Test code:

    this.iap.when(PRODUCT_ID).updated((product: IAPProduct) => {
    
    alert('is product owned:' + product.owned)
    });
  3. Use https://billing.fovea.cc/ as receipt validator service.

Note: as a workaround I use product.transaction.receipt.purchaseTime to calculate whether the subscription is still active, but not sure if purchaseTime will update when the subscription updates next month. If yes, then no worries, but product.owned still seems unreliable.

j3k0 commented 5 years ago

Calling finish() will change the product owned state. You need to call it ;-)

You can work locally on your device with the receipt's purchaseTime, but this is out of specs, not recommanded, and might break in a later version.

The recommanded (and cross-platform) way of handling the purchase flow for subscriptions is:

Setup a remote receipt validator. store.validator = "<url>";.

... Or setup a local validator function that checks the receipt locally. store.validator = function(product) { ... };. But note that local validation (vs remote) is easily tricked, there's plenty of Fake receipt generator apps for Android. So it's not the recommanded solution (see Google's Billing Best Practices for details.

For remote receipt validation, check the documentation. You can also use an hosted solution like https://billing.fovea.cc (provided by the same folk that developed this plugin, i.e. me).

j3k0 commented 5 years ago

@Nostrus Is it working now?

Nostrus commented 5 years ago

Thanks for taking the time for a detailed response @j3k0

I tried calling finish() earlier (and setting up everything according to the docs), but as far as I remember it returned false positive for the owned state, so I removed it. Maybe it was because I didn't have a validating service at the time. But now that I have been using https://billing.fovea.cc/ for a while, finish() seems to work great as well.

Thanks for this great plugin, it literally saved my project :) Keep up the great work!

Closing the issue.

odeditzhak commented 5 years ago

I've implemented the subscription flow according to the documentation: approved() calls verify() and verified() calls finish(). All works well when purchasing a subscription.

However, on Android, after purchasing a subscription, whenever the app starts, store.when('subscription').updated() is called with owned=false. It happens before approved() is triggered (which also happens every time the app starts). product.updated() sees that owned=false and cancels the subscription, giving the user a message that the subscription has expired. Then approved() is triggered, which calls verify(). Verify() validates that the subscription, calls product.finish, which in turn triggers product.owned(), which changes the user back to 'subscribed'.

So in short - every time the app starts, the user gets a message that the subscription has expired, then it get validated, and the user gets another message from owned() that they are subscribed.

Any ideas? What am I doing wrong? I'm using version 7.2.0

Suggested workaround:

Set a flag verifyCalled=true in store.validator. Then: store.when('subscription').updated(function() { if (!verifyCalled) return; // check owned..... }

Another interesting thing I've noticed - store.when('subscription').updated() is called 32 times when the app starts, and I only have 3 subscription products.

j3k0 commented 5 years ago

That's all expected. updated is called whenever one of the products field changes, and each product will go through a few different stages at initialization, so it's possible you get 32 calls.

Can can also check product.status: when it's approved => don't show a notification because it's being verified. when it's owned => it's owned. when it's valid your user isn't a subscriber.

You can also listen for the expired event to show your notification only when subscription is expired.

soniaarcdev commented 5 years ago

I have a related issue: First purch of a subs works great. Events occur. approved->verified->finished. Store says 'purchased'. Subsequent automatic renewals of the subscription cause google to send an email containing the subs purch event with the new info eg. Order number: GPA.3346-3132-9463-13430..3 Order date: 28-Aug-2019 00:21:24 BST but my updated and approved events never happen. This code is all working great on iOS. I've tried adding store.get calls to try to trigger these events. Nothing. I've tried manually purchasing the subs again. If I do that with the same code on iOS I get 'you are already subscribed' handling. On Android this give me 'error...your product is being processed'. Why are my updated and approved handlers not being called for these subs renewals although the initial purchase of the same subs works really well? (And the code works with no issues on iOS).

Any help much appreciated. Many days lost looking for play console config errors and trying new tester emails etc. etc.

hazmouneZaineb commented 4 years ago

I have a custom validation method configured for iOS, everything works fine, but if the validation failed with the code PURCHASE_EXPIRED or INTERNAL_ERROR, the product is still in the APPROVED state and does not return to the VALID state, so I cannot make a new purchase and it can't auto renew the subscription. How can i reset the product state ?

this.store.validator = async (product: IAPProduct, callback) => {
            const body: ApplePayValidateModel = {
                receipt: product.transaction.appStoreReceipt
            };
            try {
                const res = await this.service.Create(body, ApiUrl.VALIDATE_APPLE_PAY, RequestType.JSON).toPromise() as ServerResponse;
                const result = res.value as AppleValidation;
                if (result.passed) {
                    callback(true, { transaction: result.transaction });
                } else {
                    callback(false, {
                        code: result.status,
                        error: { message: "validation failed for " + product.id }
                    });
                }
            } catch (error) {
                callback(false, {
                    code: this.store.INTERNAL_ERROR,
                    error: { message: "error server => " + JSON.stringify(error) }
                });
            }
        };
this.store.when("subscription").unverified(p => console.log("unverified ", p));
hazmouneZaineb commented 4 years ago

why after registering my product and viewing store.products, I see a product with an alias 'application' (I register four products but the store.products returns five) this product is in an approved state and has a transaction, when I receive validation on my server the receipt contains the product which must be renewed automatically but the auto renew failed and the transaction expired). why after the renewal tree times the automatic renewal does not work any more the webhook does not receive any event? And after expiration, this product is still in an approved state, and I can't purchase, how can I purchase another time? i m registring four products all this products are in valid state after expiration but the store.products add a product in approve state =>

{
    additionalData: null
    alias: “application”
    canPurchase: false
    countryCode: null
    currency: null
    deferred: undefined
    description: null
    discounts: [] (0)
    downloaded: false
    downloading: false
    expired: true
    group: “”
    id: “com.test.mytest”
    ineligibleForIntroPrice: null
    introPrice: null
    introPriceMicros: null
    introPriceNumberOfPeriods: null
    introPricePaymentMode: null
    introPricePeriod: null
    introPricePeriodUnit: null
    introPriceSubscriptionPeriod: null
    loaded: true
    owned: false
    price: null
    priceMicros: null
    state: “approved”
    title: null
    transaction: {type: “ios-appstore”, appStoreReceipt:“MIIbTwYJKoZIhvcNAQcCoIIbQDCCGzwCAQExCzAJBgUrDgMCGg…  nZOGURNomxfbzMoX7KmXLPvH2iCIpoDEekQQMNtLc=“, signature: undefined}
    type: “application”
    valid: true
    version: “2.6.71"
}
Aviho-M commented 4 years ago

I have a custom validation method configured for iOS, everything works fine, but if the validation failed with the code PURCHASE_EXPIRED or INTERNAL_ERROR, the product is still in the APPROVED state and does not return to the VALID state, so I cannot make a new purchase and it can't auto renew the subscription. How can i reset the product state ?

this.store.validator = async (product: IAPProduct, callback) => {
          const body: ApplePayValidateModel = {
              receipt: product.transaction.appStoreReceipt
          };
          try {
              const res = await this.service.Create(body, ApiUrl.VALIDATE_APPLE_PAY, RequestType.JSON).toPromise() as ServerResponse;
              const result = res.value as AppleValidation;
              if (result.passed) {
                  callback(true, { transaction: result.transaction });
              } else {
                  callback(false, {
                      code: result.status,
                      error: { message: "validation failed for " + product.id }
                  });
              }
          } catch (error) {
              callback(false, {
                  code: this.store.INTERNAL_ERROR,
                  error: { message: "error server => " + JSON.stringify(error) }
              });
          }
      };
this.store.when("subscription").unverified(p => console.log("unverified ", p));

You can share your code?