Developer Products
The following example features code that would reliably handle developer product purchases through the ProcessReceipt callback. We yield the ProcessReceipt
callback until we know that the purchase was successfully saved to the DataStore or until the player leaves and triggers a profile release.
local SETTINGS = {
ProfileTemplate = {
Cash = 0,
},
Products = { -- developer_product_id = function(profile)
[97662780] = function(profile)
profile.Data.Cash += 100
end,
[97663121] = function(profile)
profile.Data.Cash += 1000
end,
},
PurchaseIdLog = 50, -- Store this amount of purchase id's in MetaTags;
-- This value must be reasonably big enough so the player would not be able
-- to purchase products faster than individual purchases can be confirmed.
-- Anything beyond 30 should be good enough.
}
----- Loaded Modules -----
local ProfileService = require(game.ServerScriptService.ProfileService)
----- Private Variables -----
local Players = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")
local GameProfileStore = ProfileService.GetProfileStore(
"PlayerData",
SETTINGS.ProfileTemplate
)
local Profiles = {} -- {player = profile, ...}
----- Private Functions -----
local function PlayerAdded(player)
local profile = GameProfileStore:LoadProfileAsync("Player_" .. player.UserId)
if profile ~= nil then
profile:AddUserId(player.UserId) -- GDPR compliance
profile:Reconcile() -- Fill in missing variables from ProfileTemplate (optional)
profile:ListenToRelease(function()
Profiles[player] = nil
player:Kick() -- The profile could've been loaded on another Roblox server
end)
if player:IsDescendantOf(Players) == true then
Profiles[player] = profile
else
profile:Release() -- Player left before the profile loaded
end
else
-- The profile couldn't be loaded possibly due to other
-- Roblox servers trying to load this profile at the same time:
player:Kick()
end
end
function PurchaseIdCheckAsync(profile, purchase_id, grant_product_callback) --> Enum.ProductPurchaseDecision
-- Yields until the purchase_id is confirmed to be saved to the profile or the profile is released
if profile:IsActive() ~= true then
return Enum.ProductPurchaseDecision.NotProcessedYet
else
local meta_data = profile.MetaData
local local_purchase_ids = meta_data.MetaTags.ProfilePurchaseIds
if local_purchase_ids == nil then
local_purchase_ids = {}
meta_data.MetaTags.ProfilePurchaseIds = local_purchase_ids
end
-- Granting product if not received:
if table.find(local_purchase_ids, purchase_id) == nil then
while #local_purchase_ids >= SETTINGS.PurchaseIdLog do
table.remove(local_purchase_ids, 1)
end
table.insert(local_purchase_ids, purchase_id)
task.spawn(grant_product_callback)
end
-- Waiting until the purchase is confirmed to be saved:
local result = nil
local function check_latest_meta_tags()
local saved_purchase_ids = meta_data.MetaTagsLatest.ProfilePurchaseIds
if saved_purchase_ids ~= nil and table.find(saved_purchase_ids, purchase_id) ~= nil then
result = Enum.ProductPurchaseDecision.PurchaseGranted
end
end
check_latest_meta_tags()
local meta_tags_connection = profile.MetaTagsUpdated:Connect(function()
check_latest_meta_tags()
-- When MetaTagsUpdated fires after profile release:
if profile:IsActive() == false and result == nil then
result = Enum.ProductPurchaseDecision.NotProcessedYet
end
end)
while result == nil do
task.wait()
end
meta_tags_connection:Disconnect()
return result
end
end
local function GetPlayerProfileAsync(player) --> [Profile] / nil
-- Yields until a Profile linked to a player is loaded or the player leaves
local profile = Profiles[player]
while profile == nil and player:IsDescendantOf(Players) == true do
task.wait()
profile = Profiles[player]
end
return profile
end
local function GrantProduct(player, product_id)
-- We shouldn't yield during the product granting process!
local profile = Profiles[player]
local product_function = SETTINGS.Products[product_id]
if product_function ~= nil then
product_function(profile)
else
warn("ProductId " .. tostring(product_id) .. " has not been defined in Products table")
end
end
local function ProcessReceipt(receipt_info)
local player = Players:GetPlayerByUserId(receipt_info.PlayerId)
if player == nil then
return Enum.ProductPurchaseDecision.NotProcessedYet
end
local profile = GetPlayerProfileAsync(player)
if profile ~= nil then
return PurchaseIdCheckAsync(
profile,
receipt_info.PurchaseId,
function()
GrantProduct(player, receipt_info.ProductId)
end
)
else
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end
----- Initialize -----
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(PlayerAdded, player)
end
MarketplaceService.ProcessReceipt = ProcessReceipt
----- Connections -----
Players.PlayerAdded:Connect(PlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = Profiles[player]
if profile ~= nil then
profile:Release()
end
end)