Purchasing the same developer product calls 'ProcessReceipt' one more time each time it is purchased

Sorry for the long title, I couldn’t think of a short way to describe the bug.

Developer product purchasing is currently plagued by a serious bug.

If you purchase a devproduct, the callback function on the server is called N+1 times, where N is the number of times you’ve purchased the devproduct.

In my game, I have a “Buy 250 Coins” developer product. The first time I buy the devproduct, ProccessReceipt is called once on the server. The second time I purchase the devproduct, ProccessReceipt is called twice on the server, then three times, etc.

This is a serious bug. It essentially allows a player to buy exponentially more currency than what they are paying for.

Here’s a tweet from skylar about this bug:
https://twitter.com/_SkylarBowen/status/942929416006586368

6 Likes

Is the purchaseId actually new for each call on every iteration of this? Or does it just re-fire the event for old purchaseIds?

You should use purchaseId to figure out if you’ve already followed up on a particular transaction. That being said, this is probably still a bug.

1 Like

Let me check my debug logs. Just a moment please.

Have you made sure to return Enum.ProductPurchaseDecision.PurchaseGranted in the callback rather than return nothing?

4 Likes

Yes, it returns Enum.ProductPurchaseDecision.PurchaseGranted.

1 Like

They have different PurchaseIDs.

So the first time it fires i.e. “a”, the second time it fires “b” and “c”, and then “d”, “e”, “f”? Or does it fire “a”, “a” and “b”, “a” and “b” and “c”?

I made a mistake (misread my logs), the Purchase IDs are the same.

It fires “a”, “b” “b”, “c” “c” “c”, “d” “d” “d” “d”, etc.

1 Like

Here’s the output from my console (I omitted the first purchase output):

[Devproduct Manager] Reshiram110 just purchased product '250 Coins' for 25 Robux. | Purchase ID : 42a0864d50a62748cf7c34fd0faab0ca

[Devproduct Manager] Reshiram110 just purchased product '250 Coins' for 25 Robux. | Purchase ID : 42a0864d50a62748cf7c34fd0faab0ca

[Devproduct Manager] Reshiram110 just purchased product '250 Coins' for 25 Robux. | Purchase ID : f927601bac15aaca76db4e3fb459db59

[Devproduct Manager] Reshiram110 just purchased product '250 Coins' for 25 Robux. | Purchase ID : f927601bac15aaca76db4e3fb459db59

[Devproduct Manager] Reshiram110 just purchased product '250 Coins' for 25 Robux. | Purchase ID : f927601bac15aaca76db4e3fb459db59

I bought the devproduct 3 times in total.

EDIT :

My ingame notification system visually displays this. In my game, when you purchase a devproduct, you’ll recieve a ‘push notifcation’ letting you know that the purchase was successful.

I get 1 notification, then 2, then 3, then 4, etc. at a time.

Could you post the code that handles the ProcessReceipt?

3 Likes

I’m not experiencing this bug in my game, I just tested it in a live server and it didn’t happen.

1 Like

The ProcessReceipt callback will be called again every time the player either joins the game or makes another purchase, until Enum.ProductPurchaseDecision.PurchaseGranted is successfully returned.

This means that if you for example have a :SetAsync call in the ProcessReciept function that takes say 15 seconds to finish, it’s possible for ProcessReceipt to be called again for that same product while it’s already being processed by the earlier callback.

As @buildthomas suggested, you have to for example keep track of all the individual PurchaseIDs in a table and check whether you’ve already started processing a purchase to prevent this.

9 Likes

I have just tested it now and I don’t experience this. Here’s an example of how you should be doing it:

local marketplaceService = game:GetService("MarketplaceService")

game.Players.PlayerAdded:connect(function(player)
	local leaderstats = Instance.new("IntValue")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player
	
	local coins = Instance.new("IntValue")
	coins.Name = "Coins"
	coins.Parent = leaderstats
end)

function marketplaceService.ProcessReceipt(info)
	local status = Enum.ProductPurchaseDecision.NotProcessedYet
	
	pcall(function()
		local player = game.Players:GetPlayerByUserId(info.PlayerId)
		
		if player then
			if info.ProductId == 0 then -- pretending to be a +250 Coins product
				player.leaderstats.Coins.Value = player.leaderstats.Coins.Value + 250
				status = Enum.ProductPurchaseDecision.PurchaseGranted
			end
		end
	end)
	
	return status
end

This is just something I made quick as an example. You can read this code and it’ll work perfectly.

Please note this is to give you an idea of how you should be doing it. You may have a callback to a module or whatever, I don’t know but I wanted to provide something quick so you get the idea.


I do want to also mention, if I comment out the “PurchasedGranted” like the example below then what you said becomes true, but if I don’t comment it out then what you said becomes false.

-- status = Enum.ProductPurchaseDecision.PurchaseGranted

What was the time frame between the purchases in the original post here? A few seconds, a few minutes, hours? Coeptus might be on the right track if the purchases happened only some seconds apart. The example you show here is trivial because no yielding happens, so there wouldn’t be any race conditions.

Either way, you have to make sure you are filtering out duplicate purchase IDs regardless. The purchase callback being fired with an old purchase ID may happen if you’re not done returning PurchaseGranted yet.

1 Like

Oh that was my fault as I didn’t see his reply. Yeah I’m aware; I didn’t include that in the script as it might have something to do with not returning PurchaseGranted, which I only included in the script because that was the focus. Sorry for not being clear about it.

Sorry for bumping a 6 month old thread, but this behaviour still isn’t documented on the wiki anywhere. The closest thing I found to an explanation was this obscure thread I came across after searching for a pretty long time. I understand its more unexpected behaviour than a bug, but a simple note warning against yielding in a ProcessReciept callback would be nice.


Even the sample on the wiki has a yielding process in the callback.

2 Likes

Yielding in the ProcessRecipet callback is necessary to save a players purchase, and is not bad practice. The key is checking that you have not already granted the purchase, as is done in the example you posted.

1 Like

Ah right I didn’t read the sample code properly, anyway, I was trying to point out how the callback would get invoked multiple times if the callback were to yield, but that isn’t really documented on the wiki anywhere.

Is it necessary to use datastores to record the fact that the purchase was granted? Is it actually possible for ProcessReceipt to be called for the same exact purchase over multiple servers? It seems like just storing the record in a table that goes away when the server ends (and doesn’t yield when written to) would be fine.

You’d still have to make other datastore requests when handling a purchase, I wasn’t aware I’d have to do it in a separate thread until recently.