Question

Invalid subscription information in didReceiveUpdated purchaserInfo delegate

  • 13 December 2021
  • 4 replies
  • 68 views

Badge +5

Hello,

We would like to report an issue we are facing with the RevenueCat framework.We are implementing the delegate to receive subscription status changes, as it’s recommended in the documentation: https://docs.revenuecat.com/docs/purchaserinfo#listening-for-purchaser-info-updates

Quite often, this delegate is called with invalid subscription information. For example, here is one callback from today, December 13th -- the app wasn’t launched since friday, and instead of fetching the new subscription status (which should be “expired”), it returned an active subscription from December 10th:

delegate update info: <PurchaserInfo
 originalApplicationVersion: 1.0,
 latestExpirationDate: 2021-12-10 16:04:57 +0000
 activeEntitlements: {
    PremiumAccess =     {
        billingIssueDetectedAt = null;
        expiresDate = “2021-12-10 16:04:57 +0000";
        isActive = Yes;
        isSandbox = Yes;
        latestPurchaseDate = “2021-12-10 16:01:57 +0000”;
        originalPurchaseDate = “2021-12-06 16:39:57 +0000";
        periodType = null;
        productIdentifier = “com.neybox.today.premium.monthly”;
        store = 0;
        unsubscribeDetectedAt = null;
        willRenew = Yes;
    };
},
 activeSubscriptions: {
    “com.neybox.today.premium.monthly” =     {
        expiresDate = “2021-12-10 16:04:57 +0000”;
    };
},
 nonConsumablePurchases: {(
)},
 requestDate: 2021-12-10 16:03:21 +0000
 firstSeen: 2021-12-09 18:13:32 +0000,
originalAppUserId: C6611DDC-E232-4936-95AC-586C7108D951,
entitlements: {
    PremiumAccess =     {
        billingIssueDetectedAt = null;
        expiresDate = “2021-12-10 16:04:57 +0000";
        isActive = Yes;
        isSandbox = Yes;
        latestPurchaseDate = “2021-12-10 16:01:57 +0000”;
        originalPurchaseDate = “2021-12-06 16:39:57 +0000";
        periodType = null;
        productIdentifier = “com.neybox.today.premium.monthly”;
        store = 0;
        unsubscribeDetectedAt = null;
        willRenew = Yes;
    };
},
>

Using the `purchaserInfo` API (https://docs.revenuecat.com/docs/purchaserinfo#get-user-information), we do get the correct, expired, subscription status.
 

manual update info: <PurchaserInfo
 originalApplicationVersion: 1.0,
 latestExpirationDate: 2021-12-10 18:06:51 +0000
 activeEntitlements: {
},
 activeSubscriptions: {
},
 nonConsumablePurchases: {(
)},
 requestDate: 2021-12-13 08:59:21 +0000
 firstSeen: 2021-12-09 18:13:32 +0000,
originalAppUserId: C6611DDC-E232-4936-95AC-586C7108D951,
entitlements: {
    PremiumAccess =     {
        billingIssueDetectedAt = null;
        expiresDate = “2021-12-10 18:06:51 +0000";
        isActive = No;
        isSandbox = Yes;
        latestPurchaseDate = “2021-12-10 18:03:51 +0000”;
        originalPurchaseDate = “2021-12-06 16:39:57 +0000";
        periodType = null;
        productIdentifier = “com.neybox.today.premium.monthly”;
        store = 0;
        unsubscribeDetectedAt = “2021-12-10 18:06:55 +0000";
        willRenew = No;
    };
},
>

We’ve implemented on our side a check for `requestDate`, ignoring old responses, but we thought to share this in case there is something we or the framework should do to make sure the delegate callback is up to date. We are using a sandbox account for this test.

Thank you!


4 replies

Userlevel 5
Badge +9

Happy new year! I’ve been unable to reproduce this behavior in a sample app. Are you seeing the same thing if you build one of our samples

  1. user launches app after subscription expired for some time
  2. delegate returns older version of purchaser info
  3. Purchases.shared.purchaserInfo  returns new version
  4. delegate is called again with older version

 

This behavior is definitely unexpected - the delegate shouldn’t be called again in #4. Do the debug logs show any warnings or clues what could be going on? Maybe two instances of Purchases are being configured somehow? Does updating to the latest SDK version resolve it for you?

Badge +5

@ryan Hello and happy new year! Any insight on this issue? Getting stale information is a serious issue and getting around it manually creates unnecessary complications to the codebase, so this is a high priority problem for us.

Badge +5

Thank you for your response @ryan .The problem we are facing is even after a new version of the purchaser info is received using Purchases.shared.purchaserInfo , the delegate is called with the older version, so we end up with a conflicting state, that is the reason why we integrated the requestDate check.The use case would be:

  1. user launches app after subscription expired for some time
  2. delegate returns older version of purchaser info
  3. Purchases.shared.purchaserInfo  returns new version
  4. delegate is called again with older version

Is this expected?

Userlevel 5
Badge +9

Hey @Neybox Digital!

If there’s any cached PurchaserInfo object, that will always be returned first to the delegate when the SDK is launched from a cold state. This is by design to keep your application fast, and allow it to work even if there’s no network connection.

If the cache is stale (>5mins old) then a network request is performed in the background to update the PurchaserInfo object in the cache. If the PurchaserInfo object changes from this background update, the delegate will fire again with the updated object. 

 

We’ve implemented on our side a check for `requestDate`, ignoring old responses,

A few works of caution with this approach:

  1. If the device goes offline and the requestDate cannot be updated you could be locking people out of your app. This may be okay for some applications, but typically locking out paying customers is a worse experience then letting a customer with an expired subscription briefly continue to access premium features.
  2. If you’re using a device timestamp to compare against the requestDate this can be easily manipulated by the end customer which could allow them to access your app forever for free.

The recommended approach is to get the latest PurchaserInfo object through the SDK as often as needed throughout the lifecycle of your app. This way, if a customer subscription expires they may still get one more app launch for “free” (if reading an old cached value) but this would quickly be updated in the background by the network and the next check of PurchaserInfo would contain the expired subscription.

Reply