Skip to main content

I’m trying to implement a gift card redemption flow in my ReactNative app. I’m testing in sandbox / staging right now. Currently, once the user inputs a code, it fires of a request to my backend, which does the call to the Grant Entitlements REST API. My backend then returns a success state, and then my frontend rechecks getCustomerInfo, but it’s not working as expected.

 

TL;DR:

  • How should I be checking for customerInfo in my ReactNative app after I grant an entitlement via the REST API? It doesn’t seem to be refreshing correctly?
  • It seems to be the case that the user’s Promo entitlement in customerInfo can’t be updated with subsequent successful calls to grantEntitlement, which means that the same user can’t redeem multiple subsequent promotions. Is this just a sandbox / staging thing, a short duration thing (I have it set for 10 min intervals for testing), or is this a production limitation?

FULL CONTEXT 

// NOTE: I had to string together multiple examples, so ignore the timestamps

In my app, I make these calls once a user has submitted their giftcode:

      // Call the grantEntitlement API
const response = await grantEntitlement(giftCardCode);

// Refresh RevenueCat data
const customerInfo = await Purchases.getCustomerInfo();
console.log('customerInfo', customerInfo);

 

My grantEntitlement API returns successfully. In fact, here’s a print out of my logs from the backend function, which shows a successful response from the RevCat REST API:

grantResult.subscriber.entitlements.premium {
expires_date: '2024-07-15T05:43:58Z',
grace_period_expires_date: null,
product_identifier: 'rc_promo_premium_custom',
purchase_date: '2024-07-15T05:33:59Z'
}

But when I call getCustomerInfo right after the API returns, I get the following:

{ allPurchasedProductIdentifiers: d],
originalApplicationVersion: null,
activeSubscriptions: a],
allExpirationDatesMillis: {},
requestDateMillis: 1721023380000,
originalPurchaseDateMillis: null,
latestExpirationDate: null,
originalAppUserId: '$RCAnonymousID:706f2861bcbf4a0a8e84ed77c7dc7ca4',
allExpirationDates: {},
managementURL: null,
allPurchaseDates: {},
nonSubscriptionTransactions: r],
originalPurchaseDate: null,
firstSeen: '2024-07-12T21:08:37Z',
latestExpirationDateMillis: null,
entitlements: { active: {}, verification: 'NOT_REQUESTED', all: {} },
requestDate: '2024-07-15T06:03:00Z',
firstSeenMillis: 1720818517000,
allPurchaseDatesMillis: {} }

so the entitlements.active is empty. But! If I log out, and then log back in, where I call Purchases.login(), I get the following customerInfo (called like this: const {customerInfo, created} = await Purchases.logIn(currentUser.username);)

{ allPurchaseDatesMillis: { rc_promo_premium_custom: 1721023422000 },
originalApplicationVersion: null,
activeSubscriptions: 'rc_promo_premium_custom' ],
entitlements:
{ active:
{ premium:
{ willRenew: false,
ownershipType: 'PURCHASED',
expirationDate: '2024-07-15T06:13:41Z',
latestPurchaseDate: '2024-07-15T06:03:42Z',
verification: 'NOT_REQUESTED',
originalPurchaseDate: '2024-07-15T06:03:42Z',
productIdentifier: 'rc_promo_premium_custom',
isSandbox: false,
latestPurchaseDateMillis: 1721023422000,
billingIssueDetectedAtMillis: null,
periodType: 'NORMAL',
isActive: true,
unsubscribeDetectedAt: null,
productPlanIdentifier: null,
store: 'PROMOTIONAL',
identifier: 'premium',
expirationDateMillis: 1721024021000,
originalPurchaseDateMillis: 1721023422000,
unsubscribeDetectedAtMillis: null,
billingIssueDetectedAt: null } },
all:
{ premium:
{ productIdentifier: 'rc_promo_premium_custom',
originalPurchaseDateMillis: 1721023422000,
willRenew: false,
ownershipType: 'PURCHASED',
latestPurchaseDate: '2024-07-15T06:03:42Z',
billingIssueDetectedAt: null,
unsubscribeDetectedAtMillis: null,
latestPurchaseDateMillis: 1721023422000,
isSandbox: false,
originalPurchaseDate: '2024-07-15T06:03:42Z',
verification: 'NOT_REQUESTED',
store: 'PROMOTIONAL',
periodType: 'NORMAL',
identifier: 'premium',
unsubscribeDetectedAt: null,
isActive: true,
productPlanIdentifier: null,
expirationDateMillis: 1721024021000,
billingIssueDetectedAtMillis: null,
expirationDate: '2024-07-15T06:13:41Z' } },
verification: 'NOT_REQUESTED' },
allPurchasedProductIdentifiers: e 'rc_promo_premium_custom' ],
firstSeen: '2024-07-12T21:08:37Z',
originalAppUserId: '$RCAnonymousID:.... ID ... ',
allExpirationDates: { rc_promo_premium_custom: '2024-07-15T06:13:41Z' },
nonSubscriptionTransactions: c],
originalPurchaseDate: null,
firstSeenMillis: 1720818517000,
allExpirationDatesMillis: { rc_promo_premium_custom: 1721024021000 },
originalPurchaseDateMillis: null,
latestExpirationDate: '2024-07-15T06:13:41Z',
requestDateMillis: 1721023578980,
latestExpirationDateMillis: 1721024021000,
requestDate: '2024-07-15T06:06:18Z',
managementURL: null,
allPurchaseDates: { rc_promo_premium_custom: '2024-07-15T06:03:42Z' } }

So I’m able to get a correct entitlement that’s active, just not when calling getCustomerInfo immediately after I grant the entitlement. 

What’s the right way to getCustomerInfo after I call grant entitlement? The rest of my app kind of relies on CustomerInfo to present premium status in the right way, and if this isn’t it, I’d like to know what to do differently. 

---

Also, after my entitlement expires, and I try to redeem another giftcode, the customerInfo reads:

{"activeSubscriptions": a], "allExpirationDates": {"rc_promo_premium_custom": "2024-07-15T05:46:52Z"}, "allExpirationDatesMillis": {"rc_promo_premium_custom": 1721022412000}, "allPurchaseDates": {"rc_promo_premium_custom": "2024-07-15T05:36:52Z"}, "allPurchaseDatesMillis": {"rc_promo_premium_custom": 1721021812000}, "allPurchasedProductIdentifiers": ""rc_promo_premium_custom"], "entitlements": {"active": {}, "all": {"premium": {Object]}, "verification": "NOT_REQUESTED"}, "firstSeen": "2024-07-10T21:38:45Z", "firstSeenMillis": 1720647525000, "latestExpirationDate": "2024-07-15T05:46:52Z", "latestExpirationDateMillis": 1721022412000, "managementURL": null, "nonSubscriptionTransactions": l], "originalAppUserId": "$RCAnonymousID:...ID....", "originalApplicationVersion": null, "originalPurchaseDate": null, "originalPurchaseDateMillis": null, "requestDate": "2024-07-15T05:47:34Z", "requestDateMillis": 1721022454000}

The previous redeemed giftcode is present, but there’s no indication of the new call to grantEntitlement, and the activeSubscriptions is empty. 

This seems to persist for a long time, where subsequent calls to grantEntitlement doesn’t seem to refresh the active state on the user’s entitlement. Also, when I log out and call Purchases.logIn, active entitlements stays empty and the old promo entitlement is still shown (this is called a minute after trying to redeem a new giftcode).

{ allPurchaseDates: { rc_promo_premium_custom: '2024-07-15T06:03:42Z' },
managementURL: null,
allPurchaseDatesMillis: { rc_promo_premium_custom: 1721023422000 },
requestDate: '2024-07-15T06:29:45Z',
activeSubscriptions: T],
originalApplicationVersion: null,
originalAppUserId: '$RCAnonymousID:...ID...',
allExpirationDates: { rc_promo_premium_custom: '2024-07-15T06:13:41Z' },
originalPurchaseDate: null,
nonSubscriptionTransactions: u],
originalPurchaseDateMillis: null,
allExpirationDatesMillis: { rc_promo_premium_custom: 1721024021000 },
requestDateMillis: 1721024985760,
latestExpirationDate: '2024-07-15T06:13:41Z',
latestExpirationDateMillis: 1721024021000,
firstSeen: '2024-07-12T21:08:37Z',
firstSeenMillis: 1720818517000,
allPurchasedProductIdentifiers: , 'rc_promo_premium_custom' ],
entitlements:
{ active: {},
verification: 'NOT_REQUESTED',
all:
{ premium:
{ ownershipType: 'PURCHASED',
willRenew: false,
unsubscribeDetectedAtMillis: null,
billingIssueDetectedAt: null,
expirationDate: '2024-07-15T06:13:41Z',
isSandbox: false,
latestPurchaseDateMillis: 1721023422000,
verification: 'NOT_REQUESTED',
originalPurchaseDate: '2024-07-15T06:03:42Z',
latestPurchaseDate: '2024-07-15T06:03:42Z',
store: 'PROMOTIONAL',
identifier: 'premium',
isActive: false,
productPlanIdentifier: null,
periodType: 'NORMAL',
unsubscribeDetectedAt: null,
originalPurchaseDateMillis: 1721023422000,
billingIssueDetectedAtMillis: null,
productIdentifier: 'rc_promo_premium_custom',
expirationDateMillis: 1721024021000 } } } }

Is it the case that, after I make the first call to grant entitlement API, subsequent calls doesn’t actually have an impact on active subscriptions? Or is this just an artifact of staging / sandbox environments. 

FWIW, I have my endMS for the granted entitlement set for 10 minutes after I initiate the call, so maybe the duration is just too short?

 

Thanks so much for your help! I’d love to push this to production soon, but I don’t want to do it in a way where I don’t understand the limitations or the flow of data. 

Hi @laura-wanderly, thanks for your detailing your issue so clearly! Customer info is cached and only periodically updates (even when you fetch customerInfo), so after granting the promotional entitlement I’d suggest that you await the `invalidateCustomerInfoCache` method, then fetch customerInfo again. This should returned fresh customerInfo. 

For the freshest customerInfo, I’d suggest setting up the listener, but it’s important to note that this listener only pushes new customerInfo when a purchase is detected on device. So in the case above, you’ll still want to invalidate the customerInfo cache.

You can grant the same entitlement to the same user multiple times, but they don’t stack (e.g. granting two one-month entitlements at the same time won't do anything). How close to the expiration date are you granting the entitlement again? If the times are very close, I wonder if there’s some kind of race condition going on. Can you experiment and let me know?

Also, you can’t grant the same promotional entitlement when an active one already exists for the customer. So you’ll have to wait until the current one expires before granting another one.

Thanks!


Thanks so much for the response! I'll give invalidateCustomerInfoCache and report back.

 

Re: granting multiple entitlements to the same user, I'm using 10 and 15 min to represent two different scenarios. This is far apart enough that I don't think it's really a race condition in the traditional sense, but I'll try it out with invalidateCustomerInfoCache and see if there is any interaction happening there.