Solved

App IDs transferring subscriptions to anonymous users

  • 2 February 2022
  • 27 replies
  • 1415 views

Userlevel 1
Badge +2

We have an app that requires the users to login before making a purchase.

We’re observing (in our sandbox testing) that occasionally a users subscription gets transferred from one of our users id’s to an anonymous id. What's quite frustrating is this is happening very infrequently and we haven’t been able to recreate it predictably.

Here is an example event (id: 0a5fc80d-ca5a-451a-8a31-cb16b95a3a92)

{
"event_timestamp_ms": 1643816650712,
"store": "APP_STORE",
"transferred_from": [
"ce3873cc-10a7-46d0-9463-43364f6b2cb8",
"$RCAnonymousID:6a5e992f89a44858b2a8effa6996f257"
],
"transferred_to": [
"$RCAnonymousID:a0490b63949a4b6e99a3bf372b3da781"
]
}

 

Our touch points with the React Native SDK are rather light:

  • We have an useEffect() that calls Purchases.setup(key) with our keys in our main App.tsx as per the examples.
  • When a user logs into our app we call Purchases.logIn(user.id) using the response to set our initial local subscription state.
  • To keep things up to date we call Purchases.getPurchaserInfo when the user brings the app back into the foreground if they are logged in and have a requestDate that's more than 5 minutes old.
  • We call Purchases.logOut() when the user hits the Logout action in our UI.

Sometimes (rarely) when a user logs out and back into our app the above style event will occur.

What I can assume (but can’t validate) is somewhere in our implementation we’re causing the SDK to transfer the purchases when the user is logged out. But I'm unclear what part of the SDK would do that?

 

The visible effect this has on us is that the user that does log in (i.e ce3873cc-10a7-46d0-9463-43364f6b2cb8 from example event). They don't have any subscription information as its been transferred to the anonymous user. Oddly when this does happen if they log out and back in again it gets transferred back to them.

 

Would appreciate any guidance here as theres a good chance we’re doing something wrong but since it works 90% of the time its difficult to pin point what may be wrong.

icon

Best answer by Andy 15 February 2022, 21:50

View original

27 replies

Userlevel 1
Badge +2

Ok so doing some more investigation we think we understand what is happening. I’m just not 100% sure if it is desired behaviour. I’ve detailed steps to try and recreate below, hoping someone with more experience will explain if this is correct behaviour.

  • Open app and call set up (We have an anonymous id here as expected)
  • User creates an account - We call login with the id
  • User subscribes, purchase recorded in RC and alias for the User ID is set against the previous anonymous id. All good so far.
  • User logs out (Purchases.logOut)
  • User backgrounds the app (or quits?)
  • User re-opens the app (still logged out) after 5 - 15 mins (the cache period)
  • RC transfers their purchase to the anonymous ID.
Example from our app

 

{
"event_timestamp_ms": 1643903636840,
"store": "APP_STORE",
"transferred_from": [
"c56acef2-039e-4a10-8ee4-ed2c1ff3772f",
"$RCAnonymousID:4fbb22f4be4b4f1abdace65eb5edb799"
],
"transferred_to": [
"$RCAnonymousID:b4ae34bbe4a04d5da4a02f85bd37a93b"
]
}

 

Our assumption going into this was that in order to transfer a purchase you had to manually with restoreTransactions. This appears to restore the transactions to whatever user ID is currently active when it enters the foreground after the cache period is over?

One rather big downside to this behaviour is if the user then logs back into their regular account the getPurchaserInfo() returns no subscription info as its all been transferred to the anonymous user. If we return around 5 - 15 minutes later the call to getPurchaserInfo() then works as the purchase has been transferred back.

 

We could be way off base here and would really appreciate some clarification on whether this is expected or not? 

Userlevel 1
Badge +2

Does anyone have any suggestions here? It’s a bit of a red flag for us at the moment. :grimacing:

 

If nothing else if anyone can confirm whether or not its the desired behaviour that the Revenue Cat SDK will automatically transfer purchases to the account that's currently logged in (even anonymous) if a user returns to our app with a different user id (or anonymous) having made a purchase previously?

If the above is true can someone confirm what role restoreTransactions() does as it seems to be happening automatically for us?

 

If this the case is our work around to never really call Purchases.logOut() and just rely on the fact that if the user logs in with a different account later, the subsequent call to Purchases.logIn() will swap them over?

 

 

Userlevel 6
Badge +8

Hey @Nizza!

Thanks for the detailed information here, and apologies for the late response.

Our SDK shouldn’t be automatically transferring purchases unless restorePurchases or syncPurchases (or logIn in some cases) is called. Just launching the app shouldn’t be transferring purchases.

Can you send over debug logs that reproduce the behavior you’re experiencing?

Userlevel 1
Badge +2

Thanks for getting back to me @cody really appreciate it.

 

I’ve managed to recreate this again following the steps above. Logs are below and I’ve also supplied a screenshot of how it manifests itself in our dashboard and the transfer event data.

 


00:54.383.801 [Purchases] - DEBUG: ℹ️ Debug logging enabled
00:54.383.916 [Purchases] - DEBUG: ℹ️ SDK Version - 3.13.1
00:54.383.964 [Purchases] - DEBUG: 👤 Initial App User ID - (null)
00:54.384.132 [Purchases] - DEBUG: 👤 Identifying App User ID: $RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f
00:54.387.152 [Purchases] - DEBUG: ℹ️ Delegate set
00:54.437.829 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/$RCAnonymousID0X0P+02d987411791d4553bce4e13eaaf3cd1f
00:54.437.928 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f
00:54.444.673 [Purchases] - DEBUG: ℹ️ There's a request currently running and 0 requests left in the queue, queueing GET /subscribers/$RCAnonymousID0X0P+02d987411791d4553bce4e13eaaf3cd1f/offerings
00:56.496.887 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/$RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f 201
00:56.529.597 [Purchases] - DEBUG: ℹ️ Sending latest PurchaserInfo to delegate.
00:56.530.394 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/$RCAnonymousID0X0P+02d987411791d4553bce4e13eaaf3cd1f, 1 requests left in the queue
00:56.531.600 [Purchases] - DEBUG: ℹ️ Starting the next request in the queue, <RCHTTPRequest: httpMethod=GET
path=/subscribers/$RCAnonymousID0X0P+02d987411791d4553bce4e13eaaf3cd1f/offerings
requestBody=(null)
headers={
Authorization = "Bearer <REDACTED>";
}
retried=0
>
00:56.535.351 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/$RCAnonymousID0X0P+02d987411791d4553bce4e13eaaf3cd1f/offerings
00:56.535.757 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f/offerings
00:56.680.966 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/$RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f/offerings 200
00:56.682.588 [Purchases] - DEBUG: ℹ️ Requesting products from the store with identifiers: {(
"headtrak_1499_3M"
)}
00:56.683.346 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/$RCAnonymousID0X0P+02d987411791d4553bce4e13eaaf3cd1f/offerings, 0 requests left in the queue
00:57.956.908 [Purchases] - DEBUG: ℹ️ Products request finished.
00:57.957.124 [Purchases] - DEBUG: 💰 Retrieved SKProducts:
00:57.957.302 [Purchases] - DEBUG: 💰 headtrak_1499_3M - <SKProduct: 0x282359250>
00:57.957.423 [Purchases] - DEBUG: ℹ️ 1 completion handlers waiting on products
01:36.709.983 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive
02:08.837.631 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive
02:19.558.720 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive
02:22.328.634 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST /subscribers/identify
02:22.328.739 [Purchases] - DEBUG: ℹ️ API request started: POST /v1/subscribers/identify
02:23.011.334 [Purchases] - DEBUG: ℹ️ API request completed with status: POST /v1/subscribers/identify 201
02:23.013.011 [Purchases] - DEBUG: 👤 Log in successful
02:23.020.396 [Purchases] - DEBUG: ℹ️ Serial request done: POST /subscribers/identify, 0 requests left in the queue
02:23.022.444 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings
02:23.022.582 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings
02:23.023.306 [Purchases] - DEBUG: ℹ️ Vending PurchaserInfo from cache.
02:23.172.924 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings 200
02:23.174.186 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings, 0 requests left in the queue
02:25.591.383 [Purchases] - DEBUG: ℹ️ Vending Offerings from cache
02:25.652.902 [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///private/var/mobile/Containers/Data/Application/80C8F453-4183-48D0-9C19-D12F278FC7A8/StoreKit/sandboxReceipt
02:25.653.020 [Purchases] - DEBUG: ℹ️ Receipt empty, refreshing
02:26.640.220 [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///private/var/mobile/Containers/Data/Application/80C8F453-4183-48D0-9C19-D12F278FC7A8/StoreKit/sandboxReceipt
02:26.641.806 [Purchases] - DEBUG: ℹ️ Attempting to check intro eligibility locally
02:26.647.225 [Purchases] - INFO: ℹ️ Receipt parsed successfully
02:26.647.618 [Purchases] - DEBUG: ℹ️ No existing requests and products not cached, starting SKProducts request for: {(
"headtrak_1499_3M"
)}
02:26.702.758 [Purchases] - DEBUG: 😻 SKProductsRequest did finish
02:26.703.081 [Purchases] - DEBUG: 😻 SKProductsRequest request received response
02:26.703.705 [Purchases] - DEBUG: ℹ️ Local intro eligibility computed locally. Result: {
"headtrak_1499_3M" = 2;
}
02:29.152.755 [Purchases] - DEBUG: ℹ️ Vending Offerings from cache
02:29.152.909 [Purchases] - DEBUG: ℹ️ makePurchase
02:29.154.327 [Purchases] - DEBUG: 💰 Purchasing product from package - headtrak_1499_3M in Offering 1499_3M
02:29.155.149 [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: headtrak_1499_3M (null) ((null)) (null) - 0
02:46.363.689 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive
02:46.377.226 [Purchases] - DEBUG: ℹ️ Vending PurchaserInfo from cache.
02:52.250.158 [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: headtrak_1499_3M 1000000964493923 ((null)) (null) - 1
02:52.254.141 [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///private/var/mobile/Containers/Data/Application/80C8F453-4183-48D0-9C19-D12F278FC7A8/StoreKit/sandboxReceipt
02:52.254.621 [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: 91df13d0-8afc-47b1-b08a-6ea3b2fff5e9
02:52.258.592 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST /receipts
02:52.258.720 [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
02:52.261.477 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive
02:52.268.645 [Purchases] - DEBUG: ℹ️ Vending PurchaserInfo from cache.
02:54.488.831 [Purchases] - DEBUG: ℹ️ API request completed with status: POST /v1/receipts 200
02:54.504.201 [Purchases] - DEBUG: ℹ️ Serial request done: POST /receipts, 0 requests left in the queue
02:54.509.894 [Purchases] - DEBUG: ℹ️ Sending updated PurchaserInfo to delegate.
02:54.519.819 [Purchases] - DEBUG: 💰 Finishing transaction headtrak_1499_3M 1000000964493923 ((null))
02:55.565.586 [Purchases] - DEBUG: ℹ️ PaymentQueue removedTransaction: headtrak_1499_3M 1000000964493923 ((null) (null)) (null) - 1
03:34.241.944 [Purchases] - INFO: ℹ️ Logging out user 91df13d0-8afc-47b1-b08a-6ea3b2fff5e9
03:34.245.541 [Purchases] - INFO: ℹ️ Log out successful
03:34.247.687 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d
03:34.247.756 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d
03:34.249.156 [Purchases] - DEBUG: ℹ️ There's a request currently running and 0 requests left in the queue, queueing GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings
03:34.815.366 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d 201
03:34.822.782 [Purchases] - DEBUG: ℹ️ Sending updated PurchaserInfo to delegate.
03:34.823.091 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d, 1 requests left in the queue
03:34.824.484 [Purchases] - DEBUG: ℹ️ Starting the next request in the queue, <RCHTTPRequest: httpMethod=GET
path=/subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings
requestBody=(null)
headers={
Authorization = "Bearer <REDACTED>";
}
retried=0
>
03:34.826.929 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings
03:34.827.146 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d/offerings
03:35.050.200 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d/offerings 200
03:35.051.307 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings, 0 requests left in the queue
20:12.432.880 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive
20:12.442.625 [Purchases] - DEBUG: ℹ️ PurchaserInfo cache is stale, updating from network in foreground.
20:12.444.852 [Purchases] - DEBUG: 😻 PurchaserInfo updated from network.
20:12.444.994 [Purchases] - DEBUG: ℹ️ Offerings cache is stale, updating caches
20:12.452.307 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d
20:12.452.496 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d
20:12.456.540 [Purchases] - DEBUG: ℹ️ There's a request currently running and 0 requests left in the queue, queueing GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings
20:12.979.590 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d 304
20:12.982.523 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d, 1 requests left in the queue
20:12.982.688 [Purchases] - DEBUG: ℹ️ Starting the next request in the queue, <RCHTTPRequest: httpMethod=GET
path=/subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings
requestBody=(null)
headers={
Authorization = "Bearer <REDACTED>";
}
retried=0
>
20:12.983.698 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings
20:12.983.791 [Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d/offerings
20:13.230.299 [Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d/offerings 200
20:13.240.377 [Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/$RCAnonymousID0X0P+0b74c17fcbc0547f08d7be11db44a9b6d/offerings, 0 requests left in the queue
20:14.597.147 [Purchases] - DEBUG: ℹ️ PaymentQueue updatedTransaction: headtrak_1499_3M 1000000964511006 ((null)) 1000000964493923 - 1
20:14.608.545 [Purchases] - DEBUG: ℹ️ Loaded receipt from url file:///private/var/mobile/Containers/Data/Application/80C8F453-4183-48D0-9C19-D12F278FC7A8/StoreKit/sandboxReceipt
20:14.608.874 [Purchases] - DEBUG: ℹ️ Found 0 unsynced attributes for App User ID: $RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d
20:14.611.035 [Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST /receipts
20:14.611.295 [Purchases] - DEBUG: ℹ️ API request started: POST /v1/receipts
20:15.446.006 [Purchases] - DEBUG: ℹ️ API request completed with status: POST /v1/receipts 200
20:15.456.812 [Purchases] - DEBUG: ℹ️ Serial request done: POST /receipts, 0 requests left in the queue
20:15.463.015 [Purchases] - DEBUG: ℹ️ Sending updated PurchaserInfo to delegate.
20:15.471.874 [Purchases] - DEBUG: 💰 Finishing transaction headtrak_1499_3M 1000000964511006 (1000000964493923)
20:16.257.493 [Purchases] - DEBUG: ℹ️ PaymentQueue removedTransaction: headtrak_1499_3M 1000000964511006 (1000000964493923 (null)) (null) - 1

Note line (20:12.432.880 [Purchases] - DEBUG: ℹ️ applicationDidBecomeActive) is where I brought the back back into the foreground. I didn’t quit the app.

 

You can see the customer details and the timeline below:

 

And the transfer event data:
ID 16c26c32-0da1-419d-b87a-1f6a19d36fd9

 

{  "app_id": “<REDACTED>",  "event_timestamp_ms": 1644317208359,  "store": "APP_STORE",  "transferred_from": [    "$RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f",    "91df13d0-8afc-47b1-b08a-6ea3b2fff5e9"  ],  "transferred_to": [    "$RCAnonymousID:b74c17fcbc0547f08d7be11db44a9b6d"  ]}

 

If theres anything else you need from me please let me know.

Userlevel 1
Badge +2

Also note if I log back in with the user that purchased the account, the purchaser info shows I don’t have any subscriptions:

DEBUG LOGS:

[Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request POST /subscribers/identify	
[Purchases] - DEBUG: ℹ️ API request started: POST /v1/subscribers/identify
[Purchases] - DEBUG: ℹ️ API request completed with status: POST /v1/subscribers/identify 200
[Purchases] - DEBUG: 👤 Log in successful
[Purchases] - DEBUG: ℹ️ Sending updated PurchaserInfo to delegate.
[Purchases] - DEBUG: ℹ️ Serial request done: POST /subscribers/identify, 0 requests left in the queue
[Purchases] - DEBUG: ℹ️ There are no requests currently running, starting request GET /subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings
[Purchases] - DEBUG: ℹ️ API request started: GET /v1/subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings
[Purchases] - DEBUG: ℹ️ Vending PurchaserInfo from cache.
[Purchases] - DEBUG: ℹ️ API request completed with status: GET /v1/subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings 200
[Purchases] - DEBUG: ℹ️ Serial request done: GET /subscribers/91df13d0-8afc-47b1-b08a-6ea3b2fff5e9/offerings, 0 requests left in the queue

Purchaser info

Object {
"activeSubscriptions": Array [],
"allExpirationDates": Object {},
"allExpirationDatesMillis": Object {},
"allPurchaseDates": Object {},
"allPurchaseDatesMillis": Object {},
"allPurchasedProductIdentifiers": Array [],
"entitlements": Object {
"active": Object {},
"all": Object {},
},
"firstSeen": "2022-02-08T10:27:29Z",
"firstSeenMillis": 1644316049000,
"latestExpirationDate": null,
"latestExpirationDateMillis": null,
"managementURL": null,
"nonSubscriptionTransactions": Array [],
"originalAppUserId": "$RCAnonymousID:2d987411791d4553bce4e13eaaf3cd1f",
"originalApplicationVersion": "1.0",
"originalPurchaseDate": "2013-08-01T07:00:00Z",
"originalPurchaseDateMillis": 1375340400000,
"requestDate": "2022-02-08T11:18:02Z",
"requestDateMillis": 1644319082000,
}
Userlevel 1
Badge +2

Hi @cody just wondering if you’ve had a chance to look at the above and any see anything odd going on?

Userlevel 5
Badge +8

@Nizza Sorry for the delayed response, and thanks so much for your patience. 

 

I believe what’s happening is:

  1. log in
  2. start subscription
  3. log out
  4. subscription renewal comes in

At step 4, when the renewal comes in, the SDK posts the receipt to our backend, but at this point, you’re an anonymous user. So the backend is transferring the receipt, to make sure that whoever’s currently using the device, even if not logged in, doesn’t get locked out. 

 

For this case, the best course of action would be to have a restore button available somewhere in your app once you’re logged in. 

 

Note that in practice in production this is fairly rare, since logging out of apps is rare in the first place, and more so when you have an active subscription. But you should still take care of this case, so providing a way for customers to restore purchases once they log back in should suffice. 

 

Hope this helps shed some light on the matter! Let me know if you have any more questions! 

Userlevel 1
Badge +2

Thanks @Andy that was just the clarification we were looking for. We were just concerned we were doing something wrong.

 

I accept this is rather unlikely to occur in production, and came about due to our internal testing doing some rather atypical paths through the application. That said - and I appreciate anonymous customers are a perfectly valid use case in RC - in our case, an anonymous user should never have a subscription. We require users to have an account prior to subscribing. Would this typically be a use case for the “Block transfers”? The warnings in the docs have put us off exploring that a bit further. 

Never-the-less it seems the path of least resistance here is to put in the restore purchase option so there’s a path to normality for the user should this happen.

Userlevel 5
Badge +8

@Nizza This does look like a use case for Block Transfers, but it’s still somewhat dangerous, because with this behavior enabled, a user could totally lose access to their purchases with no recovery mechanism (other than contacting you, and in turn you contacting us). 

So I wouldn’t recommend it unless you have a support team large enough to handle that kind of thing. 

I agree that having a restore purchase option will be the best path here, minimizing headaches and frustration for you or your users. 

Let me know if there’s any other way that I can help! 

Have a great day! 

Userlevel 1
Badge +5

Hi @Andy, we seem to be experiencing the same issue outlined in this thread.

We require our users to be logged in before they can initiate a purchase, and similarly they can only restore purchases when logged in. We have strict gating around this in our app. However, we are seeing subscription transfers from a logged in user to an anonymous user, which is impossible with any flow we have built.

We do not want to make use of Block Transfers because we would like users to be able to restore their transactions if they delete and re-create their account without deleting their subscription.

Is there any way that we can disable RevenueCat’s behaviour of transferring subscriptions to anonymous users in the background? If our user logs out of the app, we still expect their subscription to be associated with their account and not transferred to an anonymous user. All features enabled by the subscription are blocked in the app if the user is not logged in anyway. For us, the current RevenueCat behaviour is a bug that is causing us issues in our webhook handling as well as our product logic.

Userlevel 5
Badge +8

@mmmoussa those are valid points. 

 

In order to ensure that purchases don’t get synced into anonymous IDs, you should make sure that you only configure the SDK once the user has logged in. 

And after logOut, the only instance in which a purchase would be transferred to an anonymous ID would be if a subscription gets renewed while the user is logged out. 

At this point, the SDK will associate the renewal with the anonymous ID, since otherwise, the paying subscriber who logged out would have no way to access their subscription until they log back in. 

However that causes the awkward case where once they do log back in, they’d lose access. You can prevent this by calling `syncPurchases` right after `logIn` completes. This will re-grant them any purchases made on that device that they would have lost access to if a renewal happened while logged out. 

 

Userlevel 1
Badge +5

> you should make sure that you only configure the SDK once the user has logged in
 

This isn’t a good solution because from my understanding it would prevent tracking of users that don’t create an account, and so all of the conversion metrics that RevenueCat tracks would be wrong because it would only consider logged in users.

 

The way our app works is that users can use it with limited functionality without creating an account. Creating an account provides them some additional functionality for free as well as the option to purchase a paid subscription, which unlocks access to features that are only available while logged in. It is impossible for our logged out users to make use of a subscription or to purchase one. They are required to be logged in.

 

I had assumed that RevenueCat was making use of server notifications to track subscriptions and not relying on the user opening the app, but if I’m understanding your response correctly then that might not be the case? What I would like to see to meet my expectations is that subscription status is tracked by the server (with updates from the client being possible as long as it is us as the developer triggering them intentionally when the user is logged in) and that subscriptions are never transferred unless explicitly requested by the user through the “restore purchases” functionality. In lieu of that, another acceptable solution I could imagine would be to enable the Block Transfers feature but only block transfers to anonymous users, not to logged in users.

Userlevel 5
Badge +8

@mmmoussa I apologize, I had misinterpreted your previous message, thinking that your app would only work for active subscribers. 

 

If you do have functionality for users without an account, then you should configure the SDK as soon as the app is opened, you’re right. 

 

I had assumed that RevenueCat was making use of server notifications to track subscriptions and not relying on the user opening the app

This is correct. 

 

However, the SDK does observe the transaction queue on the device. If the app is open, the user has an active subscription and is logged out but a transaction goes through (which in your app would only happen when a subscription renews, or when a family member shares a subscription if Family Sharing is enabled), then that’s the case where the subscription would get assigned to the anonymous ID. 

Since the SDK doesn’t have a way to know whether the user will ever log in again, at that point the purchase is reassigned so that the user has a way to access their paid content. 

This can still be solved by having a restore purchases button (which I’d always recommend regardless), and also calling syncPurchases after logIn, which would automatically reassign the purchases back to the user if they had been reassigned during a log out. 

I hope that clarifies things a bit! Sorry about the confusion. 

Userlevel 1
Badge +5

Hi @Andy, that does clarify, but the specific piece that’s causing us a headache is that the SDK is reassigning the purchase to users who are logged out. We want to disable this. We are seeing in our dashboard chains of transfers at this point, where our logged in user makes a purchase and then that purchase ends up transferred one or more times to anonymous users. This means that if we need to investigate that user’s purchase history for purposes such as customer support, we have to load the customer in RC and then follow the links through sometimes multiple transfers across multiple anonymous users to see what happened to it afterwards. Our users already have the ability to restore their purchases in our app, so at this point the RC logic to automatically transfer the subscription is purely harmful.

Badge

Hi @Andy  and @cody we are facing the same issue. We only want purchases for logged in users. We are seeing transfer of purchases to anonymous users during testing using sandbox users. Though this case will be rare in the real world, we want to handle it. As I understand there is no configuration/setting to disable transfer of purchases to an anonymous user. For now, I am able to resolve this issue by calling `syncPurchases` after every `logIn`. In your documentation it’s mentioned that, Do not sync or restore on every app launch. I will not call it on every app launch but is it safe to call `syncPurchases` after every `logIn`?

Also what is the difference between `syncPurchases` and `restoreTransactions`? When should we use `restoreTransactions` instead of `syncPurchases`?

Badge +5

I have a similar situation, however it’s not about the renewal - it happens at the purchase:

  1. Have user A logged in
  2. Purchase a subscription
  3. Log out, log in as user B
  4. Purchase a consumable in-app purchase

Expected: no subscription for user B, subscription for user A

Actual: subscription for B, no subscription for A

This is extremely wrong, since user B is expecting to get a consumable in-app purchase effect, while they get an effect as if they bought a subscription. Subscriptions should only be transferred when explicitly asked by a user.

Badge +4

Me too, I am facing the same issue, I published a post about it before finding this post

 

Once the user signs out, the sdk is giving him an anonymous id, and then he loges back in, even if it’s the same account or different account. The anonymous id is getting stuck, and with each log out a new anonymous id is attached to him.

 

until today we have a customer who logged out and in multiple times, until he lost all the entitlements in all accounts and I had to manually give him the entitlements again on the revenuecat dashboard.

 

Badge +4

Have you found a solution around this?

 

for me, I just want the sdk to assign the correct usid after logging in again, and I can’t find a solution around it.

Userlevel 5
Badge +8

@Fahd Salloum The user should always be able to claim purchases made on the same device by calling restorePurchases. If they’re logged out, those purchases will be reassigned to their current anonymous user ID. If they’re logged in, they’ll be assigned to their known user ID. 

 

So in this particular case, providing a button that calls restorePurchases() should solve the issues for your user. This is something that I’d recommend for any app, regardless of whether or not it’s using an authentication system. 

 

@Alexander Ivkin The receipt is unique per device (more accurately, per AppStore account or Google Play account), so when purchases get reassigned, all purchases made through that account will be reassigned. When B is making a purchase in the case you described, the receipt for B includes that purchase and all other subscriptions made by the same AppStore / Google Play account, so that’s why it’s getting the stuff for A as well. 

 

@Raj the difference between those 2 methods is that syncPurchases() doesn’t refresh the receipt on device, which means that in a few cases (like if a purchase was made for the same AppStore / Google Play account but on another device) the purchase might not be synced. 

However, syncPurchases will never prompt the user to log in to their AppStore / Google Play account, so it’s safe to call programmatically. 

restorePurchases on the other hand will refresh the receipt, and may prompt for a log in (this is handled by the OS, so we can’t know whether it actually will prompt). Because of this, it shouldn’t be called programmatically - otherwise a user gets a log in prompt out of nowhere and it feels awkward. restorePurchases should ideally always be called as a result of a user action, like pressing a “Restore Purchases” button. 

 

 

 

Badge +4

@Andy I really don't get it, why this is the expected behavior?

"If they’re logged out, those purchases will be reassigned to their current anonymous user ID" Why?

Anyway, how can I workaround this behavior? If I logout, I don't want another logged user to get the paid subscription status, it should be kept to the user that has bought it... I'm already using a custom id (from my backend). I do not want to create alias for an user just because I'm logging out from my account…

 

If anybody was able to solve this, please let me know!

Userlevel 5
Badge +8

@Vitor Guimaraes I feel your pain. This is not a simple issue to solve for, and there are numerous edge cases that make it awkward from an SDK perspective. 

 

For the case of "If they’re logged out, those purchases will be reassigned to their current anonymous user ID", the situation is: 

 

  • A purchase goes through or gets restored (those are indistinguishable on our side, sadly, since either of them would come in as callbacks from StoreKit). 
  • There’s no logged in user
  • There’s no guarantee that there ever _will_ be a logged in user, they might just never log in again
  • The customer that has the device has definitely paid for something and they should get access

You can get around this by selecting the Block Restores behavior (https://docs.revenuecat.com/docs/restoring-purchases#block-restores), but we usually warn against doing this, since if that behavior is enabled, the only way that a user can re-gain access to their purchases is to log in again with their appUserID. 

 

The transfer behavior (i.e.: what we were discussing before) has some extra awkward cases, but it does have the advantage of being easy to recover from a user’s perspective - they just press the restorePurchases button and it’s done. 

 

Which one you use is up to you, and one might make more sense than the other based on each app. Hope this makes sense. 

Badge +1

I have just encountered this confusing behaviour and the more I read the more confused I seem to get.

Most of what is being said here seem to contradict https://www.revenuecat.com/docs/restoring-purchases#transfer-purchases

 

Our scenario is:

 

Steps

  1. User logs in (user_1)
  2. User subscribes
  3. User logs out
  4. User logs in with different account on same device (user_2)

 

*renewal happens*

 

Observe

The subscription is transferred from user_1 to user_2.

 

No request has been made to “restore purchases” and no new subscription attempts made. The only SDK calls made after the initial subscription are:

  1. Purchases.shared.logout()
  2. Purchases.shared.login(userId: “user_2”)

 

Surely no account transfer should be made unless a user specifically requests it using restore purchases?

 

  • iOS SDK version 4.17.3
  • Using StoreKit2
  • V2 receipts
Userlevel 5
Badge +8

@tom-mffy If you’re testing this out on Sandbox (or with StoreKit Configuration files), the most likely explanation of what you’re seeing is that after step 4, the subscription is getting renewed.

 

When that subscription gets renewed, the device posts the receipt to our backend, which then sees that the receipt had previously been owned by User 1, and proceeds to do the transfer. 

 

Note that in Sandbox, subscription durations are shortened:

(source is here)

 

And with StoreKit Configuration files, you can adjust the subscription renewal rate through Xcode. 

Badge +1

Hi @Andy thanks for the quick reply.

This is what I suspected was happening and the main source of my confusion. Why would the backend make such an assumption about the subscriptions and renewal?

 

In step 2, there is a clear act of intent by the user to assign their Apple account subscription to the account they are currently logged in to (user_1). Nowhere else in this scenario is such an intent made.

What makes it even more confusing is the existence of Purchases.shared.restorePurchases in the SDK, which would make it clear that the user wants to migrate their subscription over to user_2.

Is there a way to log a second user into revenue cat without it automatically migrating the user’s active subscription over to said account? I have tried setting autoSyncPurchases to false, but as yet, I haven’t seen any difference in behaviour.

 

I realise this might not exactly be the correct thread, seeing as I am not talking about anon users here.

Userlevel 5
Badge +8

@tom-mffy I can understand the confusion and frustration here. User identity is definitely not an easy thing to handle. 

Like I mentioned in my previous post:

You can get around this by selecting the Block Restores behavior (https://docs.revenuecat.com/docs/restoring-purchases#block-restores), but we usually warn against doing this, since if that behavior is enabled, the only way that a user can re-gain access to their purchases is to log in again with their appUserID. 

 

As for the why… When a renewal goes through on a device (which in production happens after days, weeks, months, or a full year depending on subs duration), our system needs to make a decision: 

  • grant the owner of the device with access, independently of their appUserID, and transfer if the subscription was previously tied to another appUserID (default behavior). 
  • deny the owner of the device access to the subscription, and keep it tied to the old appUserID (the “block restores” behavior). This still works, but if the user no longer has access to their previous appUserID for some reason, there is no way to recover access to the subscription, so it can be more delicate. 

We haven’t found a one-size-fits-all solution to this problem, sadly, so that’s why we provide options for behavior. But I recognize that might not be intuitive. 

Reply