Is this the right way to handle developer products?

I’m making a script that will handle the developer products purchases, but I’m not sure if this is the correct way of doing it, because I heard that you need to track the purchase history to avoid issues.

For the data I’m using Suphi’s DataStore Module.

I’m really confused, so any help is appreciated!

--// Variables
local products = {}

--// Private Functions
local function setUpDonationProducts()
	for id, amount in LeaderboardConfig.Donation.Products do
		products[id] = function(dataStore, receiptInfo, purchaseId)
			print(`add to the leaderboard {amount} robux!`)

			if dataStore:Save() == "Saved" then
				print(`successfully added {amount} robux to the leaderboard!`)
				return Enum.ProductPurchaseDecision.PurchaseGranted
			else
				print(`remove {amount} robux from the leaderboard!`)
				dataStore.Value.DeveloperProducts[purchaseId] = false
				return Enum.ProductPurchaseDecision.NotProcessedYet
			end
		end
	end
end

local function onProcessReceipt(receiptInfo)
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	local dataStore = DataService:GetDataStore(player, true) -- "true", means wait until the data is loaded
	if not dataStore then return Enum.ProductPurchaseDecision.NotProcessedYet end
	
	local purchaseId = receiptInfo.PurchaseId
	if dataStore.Value.Developer_Products[purchaseId] and dataStore.Value.Developer_Products[purchaseId] == true then
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end
	
	dataStore.Value.Developer_Products[purchaseId] = true
	return products[receiptInfo.ProductId](dataStore, receiptInfo, purchaseId)
end

--// Public Functions
function ProductService:Init()
	DataService = ModuleLoader.Get("DataService")
	
	setUpDonationProducts()
	MarketplaceService.ProcessReceipt = onProcessReceipt
end

The reason why you want to handle purchase ID is to avoid re-awarding purchase effects to players during race conditions, since ProcessReceipt does not guarantee “exactly once” processing but only “at least once” processing.

So if you don’t handle the purchase ID, and the player buys 100 coins, once in a blue moon you might get a race condition where the callback re-fires and then that player gets 200 coins (or any other multiple of 100).

The current version of your code here looks correct to me at a glance.

2 Likes

Thanks for clarifying that for me, I got it now. :slight_smile:

Just a small thing to nitpick is you should use

local purchaseId = tostring(receiptInfo.PurchaseId)

Because if purchase id is for whatever reason 1 it will erase all the other items in the table because roblox does not allow mixed tables inside the datastore

You also have a redundant if check here

if dataStore.Value.Developer_Products[purchaseId] and dataStore.Value.Developer_Products[purchaseId] == true then

You only need 1 of the 2 both are checking almost the same thing

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.