In this post https://developer.roblox.com/en-us/api-reference/callback/MarketplaceService/ProcessReceipt 's code example, the history of developer product purchases is saved in a datastore.
Why is this needed? In the code sample of the post, the first thing which is checked whenever the ProcessReceipt callback function is called, is whether the PurchaseId is already saved in the datastore, meaning that the purchase has already been handled succesfully and will not be granted again.
Why should that be checked in the first place? In which scenario is it possible that the same PurchaseId is called again even when it has been handled succesfully in the past?
Could it be because Enum.ProductPurchaseDecision.NotProcessedYet calls the callback function multiple times, even after the purchase already has been granted?
Little snipped of the code example:
-- The core 'ProcessReceipt' callback function local function processReceipt(receiptInfo) -- Determine if the product was already granted by checking the data store local playerProductKey = receiptInfo.PlayerId .. "_" .. receiptInfo.PurchaseId local purchased = false local success, errorMessage = pcall(function() purchased = purchaseHistoryStore:GetAsync(playerProductKey) end) -- If purchase was recorded, the product was already granted if success and purchased then return Enum.ProductPurchaseDecision.PurchaseGranted elseif not success then error("Data store error:" .. errorMessage) end
-- Record transaction in data store so it isn't granted again local success, errorMessage = pcall(function() purchaseHistoryStore:SetAsync(playerProductKey, true) end) if not success then error("Cannot save purchase data: " .. errorMessage) end