Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensuring webhook is called before client is notified of new entitlement #1094

Open
lukehutch opened this issue Jun 6, 2024 · 11 comments
Open
Labels
enhancement New feature or request

Comments

@lukehutch
Copy link

In my app, when the user purchases a product, the app is notified of the new entitlement, and then separately the server is notified of the new entitlement via the webhook.

However, I need a way to guarantee that the webhook call is completed before the app is notified of the new entitlement. Otherwise, if the app receives the entitlement first, the app may try to call a method on the server that the serve thinks the user should not have access too (because the webhook call hasn't been received yet, so the endpoint is not yet authorized for this user).

Is there some way to guarantee that the webhook call is always completed before the client is notified of the new entitlement? (Or is that already the behavior of RevenueCat?)

Sorry if this is a FAQ, I didn't see anything about this in the docs...

@lukehutch lukehutch added the enhancement New feature or request label Jun 6, 2024
@RCGitBot
Copy link
Contributor

RCGitBot commented Jun 6, 2024

👀 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

@HaleyRevcat
Copy link

Hi, this webhook delay could have to do with not having server notifications enabled. Can you make sure that you have these enabled for the platforms you are using? See our docs here for how to do this: https://www.revenuecat.com/docs/platform-resources/server-notifications

@lukehutch
Copy link
Author

lukehutch commented Jun 13, 2024

@HaleyRevcat thanks, I already have server notifications enabled. But I am looking for a guarantee that my app will be notified of the successful purchase only after the webhook has been successfully called. There is nothing in the docs saying that "A purchase request will only return a successful result after the webhook has been called, assuming that a webhook has been configured, and that server side notifications have been enabled".

This may already be true, but I need to know that this is a fundamental rule about how RevenueCat works.

@lukehutch
Copy link
Author

Hi, any updates? Does this guarantee hold?

The only alternative I can think of is that upon the client being notified of a successful purchase, the client should call the server and ask the server to pull down the user's purchase information if the webhook has not yet been called, so that the client can know that the server is up to date.

It seem silly to have to do that, but if no guarantee can be made that the webhook has been called before the client is notified, then there is no alternative. If the client tries to call a privileged endpoint immediately after being notified of a successful subscription, and the server has not yet been notified via a webhook, then the endpoint call will fail (in other words, there would be a race condition).

@HaleyRevcat
Copy link

Hi,
We can't guarantee which one will be delivered first, but can expect to get both around the same time.

Your alternative is the right idea, you don't want to handle both at the same time. You should wait until your server gets the webhook then the server can tell the device that it got the webhook, or vice versa where if you get the entitlement in app without the webhook, then tell your server. I believe the easiest way to go about this would be to wait for the webhook as this is more convenient.

@lukehutch
Copy link
Author

lukehutch commented Jun 21, 2024

@HaleyRevcat That is not convenient at all! In the client (the app), currently I call

await Purchases.purchasePackage(package)

and when that call returns, I check the CustomerInfo to see if the purchase succeeded or not. However, since you said the delivery order can't be guaranteed, I have to ignore the result of this awaited call (because my app server may not have the same updated ground truth about which entitlements are currently granted), then I have to have my app server wait for the webhook call (which there is a non-zero chance will never be received), then my app server has to notify the app through a push notification or websocket message (which is also not strictly guaranteed to be delivered) about the new entitlements, and then my app has to have a separate asynchonous handler receiving messages from the app server about ground truth updates. This makes the CustomerInfo returned from purchasePackage useless.

It also means that I can't update the UI to show the new entitlement info as soon as the purchasePackage call returns -- and the delay before I can update the UI may be arbitrarily long. This is a major problem, because I need to provide users with their paid entitlement functionality the instant that the purchasePackage call indicates a successful purchase, otherwise the app will provide a bad user experience, because users will think their purchase didn't succeed. If I trust the returned CustomerInfo and provide the user with the paid functionality, but they trigger a server call and the server denies the action because it has not yet been notified of the new entitlement, then the paid user experience will break. There is nothing more important to have working 100% of the time in any app that a premium paid feature.

All you would have to do to fix this on your end is await the webhook call successfully returning a 200 response before returning the result from the purchasePackage call. Then apps would know with 100% certainty that the webhook call succeeded before they were returned any successful CustomerInfo -- and the app would know that the CustomerInfo contains exactly the same ground truth about purchases and entitlements that the app server has received via the webhook.

If the webhook call fails, then the CustomerInfo should never be returned to the user -- the purchasePackage call should instead throw an exception indicating that the webhook call failed. Then the app can nudge the server to manually fetch the most recent entitlement ground truth via a call to https://api.revenuecat.com/v1/subscribers/user_$userId (API v1). I have to call that endpoint already even in the case of a successful webhook call, because the docs advise doing this.

Please escalate this, this is a very serious flaw in the design of the RevenueCat package purchasing flow, and it is a major pain to have to code defensively around this problem.

@lukehutch
Copy link
Author

lukehutch commented Jun 23, 2024

So just to clarify, this is the flow I am looking for (assuming a webhook is configured):

  1. App calls await Purchases.purchasePackage(package), which calls out to RevenueCat directly.
  2. RevenueCat calls app server via webhook, and awaits the result.
  3. Only after the webhook call returns a succesful (HTTP 200) result does await Purchases.purchasePackage return the CustomerInfo.

Currently the following flow is the only way of guaranteeing that the app and the app server have the same information on purchases as RevenueCat:

  1. App calls await Purchases.purchasePackage(package).
  2. When that call returns, the returned CustomerInfo is discarded.
  3. App call the app server, and asks the app server to call the subscriptions REST endpoint in the RevenueCat server to get purchase information. The app server stores the purchase information in the user account in the database. The app server then returns the updated purchase information to the app.
  4. When the app server receives a webhook call, the app server ignores the webhook call body, and calls out to the subscriptions REST endpoint. This may happen either before or after the app-triggered call to the same endpoint. (The webhook call is still needed, because non-purchase events may trigger a change in entitlements, e.g. a purchase is refunded. The app server will need to notify the client in these cases.)

This latter flow is much more inefficient -- there are several extra roundtrips, and the subscriptions endpoint has to be hit twice to guarantee that the server and client have the same view of the data (which is extra load on RevenueCat's servers).

await-ing the result of the webhook call before returning anything from purchasePackage, and then offering the requisite guarantee about ordering, is hopefully a very easy fix on your end.

@aboedo
Copy link
Member

aboedo commented Jun 28, 2024

@lukehutch

I see how this can be awkward, but having us delay the response until the webhook returns a success would introduce a lot of complications that could ultimately lead to a bad user experience.

For one, we don't control servers other than our own, so webhooks might take a long time to get a response, and they might fail if a customer's servers are down or misbehaving, but that doesn't in all cases mean that the user should not get access. If anything, not granting access in this case is fairly rare (although, I can see that it is what you need). Note that a user whose purchase went through but then the webhook failed is still getting charged, and their purchase was registered correctly by our servers.

Much like relying on a push notification can be complicated because delivery isn't guaranteed, webhooks suffer from the same problem since we can only control our own servers.

As for finding ways to have your server provide service only if the user has access, it's still a good idea for your server to check with ours in any case - a malicious user could be replicating network calls from the app to your server in order to get free access, and they could even do this from a non-jailbroken device.

So I'd recommend basically:

  • upon a successful purchase, call your backend endpoint, then have your backend use our rest API to ensure that the call is valid
  • you can cache the entitlements for the user so you don't need to perform this check every time
  • you can use the webhook and its contents as way to cache entitlements so if the webhook arrived before your app's request, you don't need to contact our servers

Doing this, any time the webhook arrives before success returns, there would be no extra backend calls. And when it does, there would only be one extra call to subscriptions.

Hope this helps!

@lukehutch
Copy link
Author

lukehutch commented Jul 12, 2024 via email

@EArminjon
Copy link

EArminjon commented Sep 13, 2024

For those who use Firebase claims and specially claims inside firestore security rules :
As RevenueCat didn't update claims before responding succeed you can't use your subscription right after gain it. You need to refresh first your Firebase Token. If you force user token to be refreshed once RevenueCat front sdk give you the success callback you can have an issue where the new token still didn't have the claims because RevenueCat didn't yet update it.

This is painful specially to manage subscription cancelling, renewal and real time.

@lukehutch
Copy link
Author

This is painful specially to manage subscription cancelling, renewal and real time.

This is by far the biggest problem and liability of using RevenueCat.

Please, RevenueCat developers, reconsider this bugfix request. The change is very simple: just wait to return from any API call until the RevenueCat view of the entitlements and purchases is consistent with the app store view.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants