I'm having trouble understanding the MarketplaceService callback

I’m trying to script a devproduct to allow access to a room for a certain amount of time after you buy it. Luckily, the documentation for MarketplaceService contains the whole framework to do something like this:
https://create.roblox.com/docs/reference/engine/classes/MarketplaceService#ProcessReceipt

I don’t understand how this part works though:

local handler = productFunctions[receiptInfo.ProductId]

			local success, result = pcall(handler, receiptInfo, player)
			-- If granting the product failed, do NOT record the purchase in datastores.
			if not success or not result then
				error("Failed to process a product purchase for ProductId: " .. tostring(receiptInfo.ProductId) .. " Player: " .. tostring(player) .. " Error: " .. tostring(result))
				return nil
			end

How does it work, and how could I apply it to what I want to do?

You never call it a function, it has to be:

local handler = function(_receipt, player)

Actually that is not the main reason, it’s one of the reasons but not the main. Check the script they posted (check the link) then scroll all the way down.
This is the callback:

MarketplaceService.ProcessReceipt = processReceipt

That function is what handles everything.

Then on top they have a table saved with every id as the index and a function as the value, then they call the function, they do that here:

local handler = productFunctions[receiptInfo.ProductId]

Depending on the gamepass bought it activates a different function, such as adding money.

Yes, I took the part from up above from the code example. I was wondering how the part of the code I mentioned works, so that I could easier apply it to my own situation.

More specifically, was confused how the pcall in the part I copied works.

The 2 lines of code call the funcitons above, based upon the product ID passed back by Roblox. The pcall part is a Protected Call? which if it errors, will not stop the script from continuing to run.

productFunctions[456456] = function(_receipt, player) -- Step 3
-- stuff
end

local handler = productFunctions[receiptInfo.ProductId] -- Step 2

local success, result = pcall(handler, receiptInfo, player) -- Step 1 

Step 1 calls Step 2 and passes through the info from ProcessReceipt event as a protected call and waits for 2 items to be passed back, success & result.

Step 2 uses the receiptinfo passed through to it as the ProcessReceipt event has a table of data associated with it which was passed through, but it only looks for the ProductId. It uses this to call Step 3, the productFunctions[some ProductId] so it will apply the relevant code events based upon what the player has bought.

Step 2 is a good example of flexible code that handles multiple inputs without the need for if statements to process them.

1 Like

Ok. How would I do this in my case? First, I want to fire the player so they get access to the room on the client side, then I want the script that gives them access to fire the server to start the countdown. How would I do this?

So you will need to add your own function to handle the product purchase. I would probably use CollectionService to add a tag the player then permit objects with that tag to pass through. For tracking the timer on the server, add the player to a tracking table when it is purchased and remove after cooldown expires:

-- Cooldown example
local playerTable = {} -- create an empty table to store player indexes in
local cooldownTime = 300 -- how long player have access to room for

productFunctions[123456789] = function(_receipt, player) 
	playerTable[player.Name] = tick() -- Add player to tracking table with the time dadded
-- Add CollectionService tag to player character
end

-- GAME LOOP - Continuously loop through tracking table
while true do
	for _, player in ipairs(playerTable ) do
		if tick() - playerTable[player.Name] > cooldownTime then -- Time passed > cool down time?
			 playerTable[player.Name] = nil --  remove the player from the table
			-- Add code to remove CollectionService tags from player character
		end
	end
	task.wait(1)
end

I use a format of the above to stop players from spamming remote events and functions.

As for the CollectionService side of things, there is a door exmaple on the Creator documentation CollectionService | Roblox Creator Documentation

1 Like

I’m going to have the cooldown time vary based on how long they pay for. How would I make this happen? Also, is this in the same script that handles the purchases? Also, why do you suggest using tick() instead of os.time()?

Does that mean you will have multiple game passes, each specifying a different amount of time? If yes, then either have another table holding their unique cooldown time or expand the playerTable to hold multiple items:

local playerTable = {} -- create an empty table to store player indexes in
local cooldownTable = {} -- empty table to hold unique cooldown times

productFunctions[123456789] = function(_receipt, player) 
	playerTable[player.Name] = tick() -- Add player to tracking table with the time added
	cooldownTable [player.Name] = 600 -- cooldown time unique to this ProductId
-- Add CollectionService tag to player character
end

-- GAME LOOP - Continuously loop through tracking table
while true do
	for _, player in ipairs(playerTable ) do
		if tick() - playerTable[player.Name] > cooldownTable[player.Name] then -- Time passed > cool down time?
			 playerTable[player.Name] = nil --  remove the player from the table
			 cooldownTable [player.Name] = nil --  remove the player from the table
			-- Add code to remove CollectionService tags from player character
		end
	end
	task.wait(1)
end

wrt tick(), that is interchangeable in the code with os.time(). Use whatever you prefer. I have read somewhere that tick*( is now deprecated, but not sure how accurate that is.

1 Like

Thank you! Also, is this in the receipt processing script or in a different server script?

You should only ever have one server script handling game pass and developer product purchases on the server side.

1 Like

Wait I have one more question. So I wrote the script, and I’m not sure if this will work:
Whole script:

local function processReceipt(receiptInfo)	
	local productKey = receiptInfo.PlayerId.."_"..receiptInfo.PurchaseId
	local purchased = false
	local success, result, errorMessage
	
	success, errorMessage = pcall(function()
		purchased = purchaseHistoryStore:GetAsync(productKey)
	end)
	if success and purchased then
		return Enum.ProductPurchaseDecision.PurchaseGranted
	elseif not success then
		error("Data store error:"..errorMessage)
	end
	
	local success, isPurchaseRecorded = pcall(function()
		return purchaseHistoryStore:UpdateAsync(productKey, function(alreadyPurchased)
			if alreadyPurchased then
				return true
			end
			
			local plr = Players:GetPlayerByUserId(receiptInfo.PlayerId)
			if not plr then
				return nil
			end
			
			local handler = productFunctions[receiptInfo.ProductId]
			
			local success, result = pcall(handler, receiptInfo, plr)
			if not success or not result then
				error("Failed to process a product purchase for ProductId: "..tostring(receiptInfo.ProductId).." Player: "..tostring(plr).." Error: "..tostring(result))
				return nil
			end			
			return true
		end)
	end)
	
	if not success then
		error("Failed to process receipt due to data store error.")
		return Enum.ProductPurchaseDecision.NotProcessedYet
	elseif isPurchaseRecorded == nil then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	else
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end
end

Section to focus on:

local success, result = pcall(handler, receiptInfo, plr)
			if not success or not result then
				error("Failed to process a product purchase for ProductId: "..tostring(receiptInfo.ProductId).." Player: "..tostring(plr).." Error: "..tostring(result))
				return nil
			end			
			return true

In between the end and return true of the section, it said to record the purchase. Is that done automatically, or no?

You will get a record of it in your Sales Summary on Roblox, either in the group the game was published under or under your Robux Transactions on your user account.

However, if what you are selling is a one time purchase, ie a buyable weapon the user can keep, then you need to ensure that you track that and store as part of your user data.

As you are just selling a Developer Product for access on a time limited basis then you don’t really need to record it, but it is good practice to keep a log in a Datastore so if the player has a problem, you can check the DS and confirm the did get the Dev Product issued to them as part of this script.

So it doesn’t automatically log it in the data store?

An excerpt from your script with comments:

local productKey = receiptInfo.PlayerId.."_"..receiptInfo.PurchaseId -- this is the saved DS data
-- [[
lots of other code in here...
]]--
success, errorMessage = pcall(function()
-- This checks the Datastore "GetAsync" to see if the purchase has already been recorded, ie does variable productKey exist		
	purchased = purchaseHistoryStore:GetAsync(productKey) 
	end)
	if success and purchased then
		return Enum.ProductPurchaseDecision.PurchaseGranted
	elseif not success then
		error("Data store error:"..errorMessage)
	end
	
	local success, isPurchaseRecorded = pcall(function()
-- This updates the existing data "UpdateAsync", but only if it exists
		return purchaseHistoryStore:UpdateAsync(productKey, function(alreadyPurchased)
			if alreadyPurchased then
				return true
			end
			
			local plr = Players:GetPlayerByUserId(receiptInfo.PlayerId)
			if not plr then
				return nil
			end
			
			local handler = productFunctions[receiptInfo.ProductId]
			
			local success, result = pcall(handler, receiptInfo, plr)
			if not success or not result then
				error("Failed to process a product purchase for ProductId: "..tostring(receiptInfo.ProductId).." Player: "..tostring(plr).." Error: "..tostring(result))
				return nil
			end			
			return true
		end)
	end)

In place of the “UpdateAsync”, you really want to be doiing a “SetAsync” which iwill actually save the new receipt and player info:

	local success, errorMessage = pcall(function()
		receiptHistoryStore:SetAsync(productKey, true)	-- Save the Receipt to DS
	end)

Why would the UpdateAsync not work?

I think if the key doesn’t exist then it won’t be created. You might need to verify that though. Here is the link to the documentation:

It specifically states:
“This function retrieves the value and metadata of a key from the data store and updates it with a new value determined by the callback function specified through the second parameter. If the callback returns nil, the write operation is cancelled and the value remains unchanged.”

1 Like

Oh, ok. Also, I’m testing my scripts right now, and the whole callback function isn’t even firing. Do you know why this might be happening?

Do you get the purchase prompt within Studio? If yes, then in the first line of your processReceipt(receiptInfo) function, add a print:

print("Purchase", receiptInfo)

This will at least confirm that the purchase iis received by the server. Test each part with prints as you go through and confirm each step of the script operates as you expect it to.