Open lukehutch opened 5 months ago
👀 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!
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
@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.
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).
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.
@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.
So just to clarify, this is the flow I am looking for (assuming a webhook is configured):
await Purchases.purchasePackage(package)
, which calls out to RevenueCat directly.await
s the result.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:
await Purchases.purchasePackage(package)
.CustomerInfo
is discarded.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.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.
@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:
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!
OK, so you basically confirmed that the CustomerInfo
received from
purchasing a product is more or less useless, except as a trigger for the
app to call the app server, so that the server can call the REST endpoint
to get a server-trusted view of the ground truth.
However the following suggestion is basically impossible, because there's no way for the app server to know when it receives a webhook request if that request is supposed to be paired with the previous purchase confirmation received by the app, or the next one -- and there is no way to know the time delay or ordering between the app and the app server receiving updated views of the same info:
However, there is also a second race condition here, caused by the relative ordering between the webhook call being received, and the new entitlements/purchases being written to the RevenueCat databases. Can you please confirm whether it is guaranteed that by the time the webhook call is initiated, the updated entitlement/purchase info has been written to the RevenueCat database, so that if my webhook function calls back to the RevenueCat REST endpoint (as your docs suggest, rather than parsing the webhook request itself), the latest purchase will always be included in the result of the REST call?
I am concerned that since you can't offer any guaranteed ordering between the app purchase completing and the webhook call being made, then you also won't be able to offer any guaranteed ordering between the webhook call being made and the RevenueCat database being updated for the REST call to return the latest data. If that is the case, that's an even more serious problem than the first problem!
This would all be a lot simpler if you could guarantee the following ordering:
Implementing this is trivial in any async/await-capable programming language. I would hope you would simply make this change, because it would solve every problem I have raised, and it would allow you to offer much stronger guarantees to your users about the validity of each data view and each event.
On Fri, Jun 28, 2024 at 3:45 PM Andy Boedo @.***> wrote:
@lukehutch https://github.com/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!
— Reply to this email directly, view it on GitHub https://github.com/RevenueCat/purchases-flutter/issues/1094#issuecomment-2197699483, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAGGCKIYCMJG2KYS5IZE3D3ZJXKRNAVCNFSM6AAAAABI4CLSW6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCOJXGY4TSNBYGM . You are receiving this because you were mentioned.Message ID: @.***>
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.
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.
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...