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?
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:
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.
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.
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.
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.
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.
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)
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.”
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.