chargebee / chargebee-flutter

MIT License
5 stars 8 forks source link

How does `restorePurchases()` determine the correct chargebee customer? #79

Open ciriousjoker opened 9 months ago

ciriousjoker commented 9 months ago

As per ios sdk documentation:

let customer = CBCustomer(customerID: "Test123",firstName: "CB",lastName: "Test",email: "cbTest@chargebee.com")
CBPurchase.shared.restorePurchases(includeInActiveProducts: true, customer: customer) { result in
      switch result {
      case .success(let response):
        for subscription in response {
          if subscription.storeStatus.rawValue == StoreStatus.Active.rawValue{
            print("Successfully restored purchases")
          }
        }
      case .failure(let error):
        // Handle error here
        print("Error:",error)
      }
    }

The ios sdk requires a customer, but the flutter sdk doesn't. How is that possible?? If we call this function without a customer, which customer is the subscription restored to?

ciriousjoker commented 9 months ago

Speculation: This is kind of important, because calling this could result in a new customer being created in Chargebee because the subscription is being imported without the existing customer.

ciriousjoker commented 9 months ago

Can someone please look at this?

cb-anurags commented 9 months ago

@ciriousjoker Thanks for raising the issue, we will fix this for flutter soon.

ciriousjoker commented 9 months ago

@cb-anurags I have one important question about this as well:

cb-haripriyan commented 9 months ago

@ciriousjoker By "call the store's native restorePurchases functionality", do you refer to the Chargebee Android and iOS SDKs' Restore purchases method (with the customer object)? If yes, then it should pretty much work as expected. If not, can you elaborate on the methods you are using?

ciriousjoker commented 9 months ago

@cb-haripriyan

Im specifically talking about this function: https://pub.dev/packages/in_app_purchase#restoring-previous-purchases

We call that package's restore purchase functionality which gives us all the receipts in a stream. This is our way to ensure that users get access to the app even chargebee has the incorrect status.

In the past we tried relying on Chargebee alone but that lead to trust issues because lots of customer complained due to incorrect data and we therefore decided to keep the fallback mechanism permanently.

cb-haripriyan commented 9 months ago

@ciriousjoker That method should restore the user subscriptions and return the stream of purchase tokens/receipts. But how do you sync these receipts to be stored in Chargebee?

ciriousjoker commented 9 months ago

@cb-haripriyan We don't. Currently we only store them in-memory to have a guaranteed to be accurate subscription status inside the app. Also, we store the latest receipt inside our database.

ciriousjoker commented 9 months ago

I was just wondering if the stream is also picked up by the Chargebee sdk and since we don't pass a customer id anywhere it might break something.

ciriousjoker commented 9 months ago

@cb-haripriyan After the general call, iirc the conclusion was that this might be causing issues because Chargebee doesn't get the uid if the stream is triggered by us and not via the Chargebee method. Do you have an ETA of when you know if this is causing issues and when a fix will be available?

cb-haripriyan commented 9 months ago

@ciriousjoker Looking at the implementation of the native restore method of flutter package (https://pub.dev/packages/in_app_purchase#restoring-previous-purchases), the android restore method is synchronous - That is the native method would receive the restored purchases receipt. This doesn't affect the subscription in Chargebee. However the iOS method is async and the response of the restored purchases reach the SKPaymentTransactionObserver listener which is registered inside the Chargebee iOS SDK. Hence the internal CB SDK listener would the receipt and it tries to resolve it to the customer which wouldn't be set. This will result in the restored subscription being moved to a customer with ID which is that of the subscription ID.

We have released a patch today with the 0.4.2 version, which takes in the customer object and restores the subscriptions. Would you be able to give it a try and let us know if this works?

ciriousjoker commented 9 months ago

@cb-haripriyan perfect, thanks for the investigation and the quick fix!

We'll roll out an update over the coming days. Unfortunately it'll be hard to evaluate if the error comes back since there's no way to find affected users directly, users just contacted our support with the issue.

ciriousjoker commented 9 months ago

@cb-haripriyan I've reviewed the v1.0.28 of the ios sdk as well as the v0.4.2 of the flutter sdk. I couldn't find any code that prevents the ios callback from being called in cases where the restorePurchases() is called via the in_app_purchase package.

What am I missing here. We cannot call Chargebee's sdk method directly, we need to call the method from in_app_purchases, at least for now. And if I understood you correctly, in those cases, on iOS (not Android), the purchase receipts are still handled by Chargebee's sdk and will be imported as new customers in Chargebee since Chargebee's restorePurchases() is never called.

cb-haripriyan commented 9 months ago

@ciriousjoker The version 0.4.2 fixes the originally raised issue which was that the restorePurchases method of the Chargebee Flutter SDK doesn't allow the passing of a Customer object. Now the restorepurchase method of Chargebee takes in a customer and when it is invoked it would restore the subscriptions correctly to the customer.

Now with regards to using in_app_purchases library along side the Chargebee SDK - since both the libraries are for similar functionality, the use of one along with the other can cause effects. That is, when you call the restore purchases method of Chargebee SDK, the in_app_purchases SDK listener will also receive the events as well, since both the SDKs register their listeners on initialization. (This is behavior in iOS since the responses are handled via delegates/listeners).

Also when the restorePurchases method of Chargebee SDK is called, the in_app_purchase listener would also be called (for iOS).

A couple of workarounds I can think of right now:

  1. For Android, call restorePurchase of both SDKs (for Chargebee method, pass the customer object). For iOS, call the restorePurchase of Chargebee SDK (with customer). The in_app_purchases listener would still receive the response since the listener is already set.
  2. After calling the restorePurchase of in_app_purchases and receiving all responses, you can follow it up a Chargebe SDK call with customer object which should sync the correct state. This however would cause back and forth changes of customer ID. We are looking at ways to prevent the Customer ID change, which should atleast prevent changing the customer to a default value.
ciriousjoker commented 9 months ago

@cb-haripriyan The workaround seems like it should work and we'll try that.

However, one change I would suggest for the Chargebee sdk on iOS is this:

This would make the sdk more robust. Unfortunately we can't fork the iOS sdk directly, but I can create a PR nonetheless if you'd like me to.

cb-haripriyan commented 8 months ago

@ciriousjoker Thanks for the input. This is indeed one of the ways we were considering. However we would need to take into account the overall SDK and parity of features b/w ios and andriod (and not just the restorePurchase method for iOS). We are considering few alternative approaches, which we solve all cases like these.