Question

Sync issues with customerInfo() after subscription expiration

  • 4 November 2023
  • 11 replies
  • 96 views

Badge +5

In a test react native application on Android, I have made a purchase and am able to see that inside the customerInfo() response. The managementURL open the Subscription Play Store page where I can see it. Now I waited for it to expire and confirmed that it has indeed expired (got the Firebase extension to deliver the EXPIRATION and CANCELLATION same time). When calling customerInfo() a few minutes after this, it still shows active entitlements. Now the same managementURL point to an empty page in Play Store so it creates user confusion.

Any idea how long or why the sync between EXPIRATION and customerInfo() exists?


11 replies

Badge +5

Please not that the same is happened when I get a SUBSCRIPTION_PAUSED event. In this case I would except the entitlements to be removed, but customerInfo() still lists active ones..

Badge +5

FWIW, I looked at the docs here and here, but not able to see why it is still cached and outdated data behind customerInfo(). I have waited more than 1 hour without and no changes (data still shows active entitlements). I’ve called customerInfo() multiple times after. I’ve also tried calling invalidateCustomerInfoCache() and this just hangs and does not resolve the promise in react native. One thing to note is that the firebase back-end is correct, it shows the right claims removed now as the side-effect of the EXPIRATION  event I assume. 

Userlevel 3
Badge +8

Hi,

Happy to help here. The customerInfo object should be updated as soon as we send that EXPIRATION webhook. Can you print out the object and paste it here? Is the .active property set to true, or are there just entitlements in the array (but with an expiration date in the past)?

 

Badge +5

yes sure. but I am in react native so I dont see an active boolean. just objects.
here is the output (names redacted). basically I make the purchase with the test account → go into OS and cancel it → then when I see expiration came from the webhooks → go into device and call customerInfo() again and still see active subscriptions for a few minutes. Eventually it does catchup..

{
"activeSubscriptions": [
"v1_premium_6mo"
],
"allExpirationDates": {
"v1_premium_1mo": "2023-11-09T04:20:39Z",
"v1_premium_6mo": "20 23-11-10T00:35:12Z"
},
"allExpirationDatesMillis": {
"v1_premium_1mo": 1699503639000,
"v1_premium_6mo": 1699576512000
},
"allPurchaseDates": {
"v1_premium_1mo": "2023-11-09T04:17:39Z",
"v1_premium_6mo": "2023-11-10T00:17:12Z"
},
"allPurchaseDatesMillis": {
"v1_premium_1mo": 1699503459000,
"v1_premium_6mo": 1699575432000
},
"allPurchasedProductIdentifiers": [
"v1_premium_6mo",
"v1_premium_1mo"
],
"entitlements": {
"active": {
"ent_premium": [
Object
]
},
"all": {
"ent_premium": [
Object
]
}
},
"firstSeen": "2023-10-25T05:29:51Z",
"firstSeenMillis": 1698211791000,
"latestExpirationDate": "2023-11-10T00:35:12Z",
"latestExpirationDateMillis": 1699576512000,
"managementURL": "https://apps.apple.com/account/subscriptions",
"nonSubscriptionTransactions": [

],
"originalAppUserId": "_my_custom_id_",
"originalApplicationVersion": "1.0",
"originalPurchaseDate": " 2013-08-01T07:00:00Z",
"originalPurchaseDateMillis": 1375340400000,
"requestDate ": "2023-11-10T00:26:34Z",
"requestDateMillis": 1699575994000
}

 

Badge +5

here is the full object coming out of customerInfo()
the pattern I noticed is that always second call to customerInfo() is correct but first one after expiration is not even if waiting longer..

{
"allPurchasedProductIdentifiers": [
"v1_premium_6mo",
"v1_premium_1mo"
],
"allExpirationDates": {
"v1_premium_1mo": "2023-11-10T03:49:09Z",
"v1_premium_6mo": "2023-11-10T00:35:12Z"
},
"originalApplicationVersion": "1.0",
"originalAppUserId": "_CUSTOM_",
"firstSeen": "2023-10-25T05:29:51Z",
"managementURL": "https://apps.apple.com/account/subscriptions",
"requestDateMillis": 1699587926000,
"allPurchaseDatesMillis": {
"v1_premium_6mo": 1699575432000,
"v1_premium_1mo": 1699587969000
},
"allExpirationDatesMillis": {
"v1_premium_6mo": 1699576512000,
"v1_premium_1mo": 1699588149000
},
"latestExpirationDateMillis": 1699588149000,
"allPurchaseDates": {
"v1_premium_1mo": "2023-11-10T03:46:09Z",
"v1_premium_6mo": "2023-11-10T00:17:12Z"
},
"requestDate": "2023-11-10T03:45:26Z",
"entitlements": {
"all": {
"rc_ent_premium": {
"isActive": true,
"productIdentifier": "v1_premium_1mo",
"isSandbox": true,
"latestPurchaseDate": "2023-11-10T03:46:09Z",
"latestPurchaseDateMillis": 1699587969000,
"expirationDateMillis": 1699588149000,
"billingIssueDetectedAt": null,
"expirationDate": "2023-11-10T03:49:09Z",
"identifier": "rc_ent_premium",
"willRenew": true,
"periodType": "NORMAL",
"originalPurchaseDateMillis": 1699239095000,
"productPlanIdentifier": null,
"unsubscribeDetectedAt": null,
"unsubscribeDetectedAtMillis": null,
"ownershipType": "PURCHASED",
"billingIssueDetectedAtMillis": null,
"store": "APP_STORE",
"originalPurchaseDate": "2023-11-06T02:51:35Z"
}
},
"active": {
"rc_ent_premium": {
"originalPurchaseDate": "2023-11-06T02:51:35Z",
"originalPurchaseDateMillis": 1699239095000,
"billingIssueDetectedAtMillis": null,
"latestPurchaseDate": "2023-11-10T03:46:09Z",
"latestPurchaseDateMillis": 1699587969000,
"expirationDateMillis": 1699588149000,
"billingIssueDetectedAt": null,
"expirationDate": "2023-11-10T03:49:09Z",
"identifier": "rc_ent_premium",
"productIdentifier": "v1_premium_1mo",
"willRenew": true,
"periodType": "NORMAL",
"productPlanIdentifier": null,
"unsubscribeDetectedAt": null,
"unsubscribeDetectedAtMillis": null,
"ownershipType": "PURCHASED",
"isSandbox": true,
"store": "APP_STORE",
"isActive": true
}
}
},
"originalPurchaseDateMillis": 1375340400000,
"activeSubscriptions": [
"v1_premium_1mo"
],
"latestExpirationDate": "2023-11-10T03:49:09Z",
"firstSeenMillis": 1698211791000,
"nonSubscriptionTransactions": [

],
"originalPurchaseDate": "2013-08-01T07:00:00Z"
}

 

Userlevel 3
Badge +8

Ah, this sounds like it’s caused by the customerInfo cache.

 

We do have a method to invalidate the cache if you want to use that

Badge +5

Thank you for your reply but this does not help @Ryan Glanz since in my comment above I’ve talked about both points and the issues I am still having. 

Again, I looked at invalidate cache and it does not work plus documentation says not to use. Is there a way to force reload the customerInfo? The method does not accept any parameters (react native sdk). I have things on the back-end that need to happen when a webhook expiration fevent happens. as well as the user in the client needs to see things correctly. if these 2 are too far apart it can cause confusion for both user and developer. The documentation says the cache if max of 5min but that was not the case in my testing. The webhook event came in and it took a long time for customerInfo() to reflect not more active entitlements. One time I got tired and even had to reinstall the app.

Badge +5

I am still unable to get invalidateCustomerInfoCache() to resolve. I was planning to compare the back-end to see if the EXPIRATION webhook was called and if the client is still showing active entitlement to call invalidateCustomerInfoCache() and then customerInfo() right after to pull latest data. But I just can’t get past clearing the cache.

Any ideas @Ryan Glanz ?

 

Userlevel 3
Badge +8

Hi,

Ah sorry, I missed the invalidateCache info in your prior comment.

I’m wondering if the EXPIRATION hasn’t made its way through the Sandbox environment → RevenueCat yet (i.e., it is not reflected in our data), or if it is and this is purely a customerInfo cache thing.

To check, if you make and cancel a purchase, then use our GET /subscribers api endpoint (you could just use curl on a terminal or postman or something), is that customerInfo object that gets returned correct? Then if you call getCustomerInfo in the SDK, is that still outdated?

That will let us know if the underlying data is correct. It sounds like it should be based on the Firebase state.

Next, do you have our customerInfo listener implemented? That should keep the customerInfo object up to date with purchases/cancellations without needing to manually invalidate.

You could also set up server notifications, which allow RevenueCat to get information from Apple even faster.

Badge +5

Thanks @Ryan Glanz
The API returns the same objects as the customers document collection from the firebase extension. So it matches what the back-end already has. To know if it is correct/accurate that is a different question. This objects does not have like an “active” entitlement node like the client SDK. It just has expiration dates in UTC so those are the same before and after purchase expired.

FWIW, I am already using server notifications and I’ve confirmed they are timely. 

I will look into customerInfo listener, but that is just an event callback for when the data changes right? Not really tied to how and when the cache gets updated. I am not really looking to react to changes in customerInfo, I just want customerInfo to return most up to date info when called since it is being called on screens open etc.

Badge +5

I was able to confirm it is a client cache issue. 

After EXPIRATION the client still shows active entitlements. But the back-end has a null management_url now as well as the revenue cat API (so that tells me the backend is correct). The expiration dates are in the past too. So only the client still shows active with past expiration dates. The managementURL on client is also populated.

I just can’t get this cache to clear and it’s totally making this out-of-sync issue bad. Maybe this is only in debug mode with timers or some other issue but I am not sure if I should just trust it and go to production..

Reply