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

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.

6 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.

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.

The report is appreciated.
Please avoid ROBLOXCRITICAL unless the house is on fire.

10 Likes

Apologies, I was not aware this wasn’t “ROBLOXCRITICAL” material.

For the future

4 Likes

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.

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.

I wouldn’t be comfortable returning that the purchase was granted until it was actually processed successfully.

Yeah, this could definitely use more clear documentation about what guarantees are made.

It is possible for ProcessReceipt to the called for the exact same purchase over multiple servers, but if you have returned PurchaseGranted the chances of this happening would be insanely small (only would happen if the web was not able to receive the purchase granted request). I think recording purchases in this way is reasonable because worse case you just give someone a little more than they paid for.

1 Like

YES! we need more Documentation!

1 Like