Question

Block Restores Not Working As Expected


Badge +2

This is the behavior I’m seeing (tested on iOS) when setting the Restore behavior to Block Restores.

  1. App store userA logs into our app as user1
  2. user1 purhcases a monthly subscription
  3. user1 eventually cancels monthly subscription and it expires
  4. user2 logs in on the same device (i.e. still app store userA)
  5. user2 purchases monthly subscription, gets the ‘receiptAlreadyInUse’ error, HOWEVER, the purchase still went through successfully and user1’s purchase was restored.

This feels like a bug that the purchase made it through even though the SDK returned an error.

Also, the link to the documentation on restores is currently pointed to here which gives a 404.


11 replies

Userlevel 5
Badge +9

Hey @jordan.corbman!

That behavior sounds expected.

RevenueCat cannot block a purchase from going through with Apple, the “Block Restores” will prevent RevenueCat from transferring the subscription or merging it with another App User ID on RevenueCat servers.

Sorry for any confusion here! Here’s a link to the restoring purchases doc for reference, I’ll see if we can clarify this behavior there: https://docs.revenuecat.com/docs/restoring-purchases

Badge +2

Wow, this is bound to create a lot of confusion for users. In the scenario I outlined above user2 receives an error, but successfully resubscribed for user1 (who probably had no intention of resubscribing).

We require our users to log in to our platform and we use their app user ID as their revenueCat ID. We would like to lock the app down so that only the user (our user) who made the purchase can use the purchase. How would you recommend we do that (and not allow users to accidentally buy things on someone else’s account). The issue stems from the fact that many families see devices like iPads as a shared device, not a personal device.

Userlevel 5
Badge +9

One thing to remember is that user1 and user2 are most likely the same human. User2 would need to confirm the purchase with the same FaceID/TouchID as user1.

My recommendation would be to show a message to user2 that they already have an account registered with your app, and they’ll need to log in with that account to access their purchased content.

if (ReceiptAlreadyInUserError) {
alert("Looks like you already have a subscription associated with another account. Log in with that account to access.")
}

Alternatively, you could set the restore behavior to transfer, which would automatically move the subscription from user1 to user2. 

Badge +1

I’ve posted a relatively similar question regarding purchases with “Block Restores” behaviour. @jordan.corbman, I’m glad that you’ve got an answer.

 

It sucks that we can’t get the best of both worlds.

 

While I was waiting for an answer to my question, I was thinking of what could be done to at least mitigate this issue and block making purchases altogether. The best that I could come up with was using CloudKit and partially storing some user’s data after making a successful purchase (user’s ID and/or an email), just have to make sure that before every `makePurchase` call you check whether or not the user is eligible for a purchase (current user ID == stored user ID, etc.).

Userlevel 5
Badge +9

Hey @Artyom Titov,

The best that I could come up with was using CloudKit and partially storing some user’s data after making a successful purchase (user’s ID and/or an email), just have to make sure that before every `makePurchase` call you check whether or not the user is eligible for a purchase (current user ID == stored user ID, etc.).

This is similar to what’s already going on with the App User ID in RevenueCat. Presumably you’re checking that the App User ID does not already have an active subscription before allowing them to purchase.

One workaround could be to call the .restorePurchases() method when the customer taps the ‘buy’ button. If the response of .restorePurchases() comes back with a “Receipt already in use” error then show the customer an error message, if it does not then you can proceed calling .makePurchase(). Note that I would only attempt this if your restore behavior is set to “Block”, not “Transfer”.

Downsides here could be that the restore will introduce additional lag, and it could prompt the customer to verify/login with their Apple ID. I could see this potentially confusing customers if they received two login prompts (1 for the restore 1 for the purchase). Could be something to experiment with though! 

My recommendation would still be to handle this through some UX and a case-by-case basis through your support team since it should only occur for a small subset of transactions.

Badge +1

Downsides here could be that the restore will introduce additional lag, and it could prompt the customer to verify/login with their Apple ID. I could see this potentially confusing customers if they received two login prompts (1 for the restore 1 for the purchase). Could be something to experiment with though! 

That’s exactly the reason why I was proposing to use CloudKit! 

It’s free, it syncs between devices linked to the same Apple ID, and you don’t have to deal with possible Apple’s confirmation dialogs for restoring purchases.

Don’t think there’s any better workaround than that, imo. Although I would appreciate if this case could be covered by RevenueCat’s SDKs internally. 

Badge +2

@ryan we can definitely message the user that this has happened, but by that point the damage is done and they would have to jump through hoops to cancel that purchase from their app store settings page, then sign into the app store with a different user. All of this causes frustration for our users that is aimed squarely at us.

I do like the thought of checking first using the .restorePurchases() method. I’ll have to see what the user experience is like in that scenario.

Unfortunately I don’t think user1 and user2 are usually the same user. From the users that have contacted us through customer support it seems like at least 50% of the time it’s just a shared family device and the family members are used to just blindly clicking through the purchase flow. Many users don’t think about the fact that there is not a 1:1 relationship between app store user and our app user.

We’re going to stay away from the transfer behavior precisely because of what I mentioned above. Our subscriptions are not intended to be family plans and in the scenario above, the purchase would just continually transfer from user1 to user2 depending on who was signed in at the moment.

@Artyom Titov , I’m working on one more track to try and solve this. Since the receits are stored on the device, I believe we can natively implement some code to get the receipt, hopefully from that we can determine if that receipt should be linked to the logged in user. I’ll let you know if I have any luck with that.

Userlevel 5
Badge +9

So there’s cases where customers may be purchasing / upgrading from outside of your app as well through the subscription management page. This is one of the reasons why we haven’t build this into the RevenueCat SDK is there’s no way for a developer to intercept and stop a purchase initiated from outside of their app. There’s some other edge cases that make it impossible to prevent this in 100% of cases but you can start layering in additional checks like you’ve brought up which would help.

 

From the users that have contacted us through customer support it seems like at least 50% of the time it’s just a shared family device and the family members are used to just blindly clicking through the purchase flow. 

I thought that every purchase needed to be confirmed with FaceID/TouchID so family members couldn’t blindly click and buy something. Turns out that’s the default behavior but there is an iOS system setting to disable this and allow purchases without authentication. 

 

Since the receipts are stored on the device, I believe we can natively implement some code to get the receipt, hopefully from that we can determine if that receipt should be linked to the logged in user. I’ll let you know if I have any luck with that.

This is very similar to what a call to .restoreTransactions() would do, except you could not refresh the receipt on the device if it doesn’t exist which should prevent the login prompt from appearing. 

Badge +2

I have read the answers above but I think I need a little more clarification. I am trying to do a subscription purchase and subscription restore purchase on the same phone but with different accounts and user IDs with block restore activated.

I have a user authentication feature where the users need to verify their email accounts before they can purchase any subscriptions.

Scenario: 

  • Client A is logged in as the primary account on Play Console or App Store.
  • Client A logs on the app and makes a subscription purchase.
  • Client A logs out of the account and Client B borrows the phone.
  • Client B logs on the app and Client B wants to purchase a subscription for his account.
  • Client A allows Client B to do the subscription purchase for Client B and then the “Duplicate Receipt Error Occurs”

Given that Client A and Client B has separate email accounts and Revenue Cat ID’s, shouldn’t the purchase go through and the customer transaction be saved on the logged on account’s User ID on Revenue Cat?

Findings:

  • It seems like “single App User ID” is meant for “the only account who’s signed in on the Play Console and App Store” and not “any verified accounts who was logged in using Purchases.logIn()”.
  • What if the “owner of the device” has different accounts on a game app where they can save their progress and purchase different subscriptions depending on what account has been logged on?
  • “Will make sure that transactions never get transferred between accounts”  I thought this depends on the current logged in user’s account by calling “Purchases.logIn()” method on the app  and not the logged in account on the play store or app store.
Requires all customers to create an account before purchasing, and has strict business logic that requires purchases to only ever be associated with a single App User ID.

Block restores. Will make sure that transactions never get transferred between accounts. Your support team should be prepared to guide customers through an account recovery process if needed.

Badge +3

@ryan solution of .restoreTransactions() does the trick for me for now.  understand the revCat product is evolving so my only ask is that this scenario be addressed in the Customer section more formally when your group has time.  i’ve used the proposed solution so at least customers don’t end up buying/renewing the subscription for a different user (i get that they are human and likely not to do this...but it’s an edge case that i’m pretty fearful of).  for now, if restoreTransactions() throws an error AFTER they click my buy button...i simply change the UX to remove the buy button and alert them to the fact they need to buy on their own device with their own apple id...

Badge +3

to complete the results from my using .restorePurchases() (that’s the new name in 5.0.1) although it worked very well to create a better UX for this edge case, my app was rejected repeatedly ONLY at the app review stage (passed TestFlight app review for external testers every time...probably less robust test if any).  Turns out that even though the issue never showed up in any other tests (with multiple beta users outside company as well) the dreaded ‘the receipt is missing’ was causing .restorePurchases() to error out (i eventually put things in place to catch it better) ONLY in the final app review.

The revCat docs outline the flaky nature of receipts and this line in particular finally turned on the lightbulb for me https://www.revenuecat.com/docs/errors

In Sandbox, you may have to make a purchase to generate a receipt before attempting the operation. 

The issue was the edge case above...the person had NOT made a purchase yet so there (in Sandbox...for this particular tester) had no receipt.  No receipt when you hit .restorePurchases() errors out.  I’ll likely restore this fix into my app once i can test it better and catch that ‘no receipt’ scenario without it causing an issue.  Hope that helps someone.

Reply