Skip to main content

Developer Products

The following example shows how you would handle developer product purchases:

-- DataTemplate.luau

local dataTemplate = {
PurchaseHistory = {},

Coins = 0,
}

export type template = typeof(dataTemplate)

return table.freeze(dataTemplate)
-- DevProducts.luau

local DataKeep = require(path_to_datakeep)
local DataTemplate = require(path_to_datatemplate)

local devProducts = {
[product_id_here] = function(player: Player, keep: DataKeep.Keep<DataTemplate.template, {}>)
keep.Data.Coins += 100

print(`{player.Name} purchased some coins!`)
end,
}

return devProducts
-- SetProcessReceipt.luau

local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local DataKeep = require(path_to_datakeep)
local DataTemplate = require(path_to_datatemplate)
local DevProducts = require(path_to_devproducts)

local purchaseHistoryLimit = 50

local function setProcessReceipt(store: DataKeep.Store<DataTemplate.template, {}>, keyPrefix: string)
local function processReceipt(receiptInfo): Enum.ProductPurchaseDecision
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)

if not player then
return Enum.ProductPurchaseDecision.NotProcessedYet
end

local isLoaded, keep = store:LoadKeep(keyPrefix .. player.UserId):await()

if not isLoaded then
return Enum.ProductPurchaseDecision.NotProcessedYet
end

if not keep then
return Enum.ProductPurchaseDecision.NotProcessedYet
end

if not keep:IsActive() then
return Enum.ProductPurchaseDecision.NotProcessedYet
end

if not keep.Data.PurchaseHistory then
keep.Data.PurchaseHistory = {}
end

if table.find(keep.Data.PurchaseHistory, receiptInfo.PurchaseId) then
-- the purchase has been added to the player's data, but it might not have saved yet
local success = keep:Save():await()

if success then
return Enum.ProductPurchaseDecision.PurchaseGranted
else
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end

-- remove purchaseIds which are beyond the limit
while #keep.Data.PurchaseHistory >= purchaseHistoryLimit do
table.remove(keep.Data.PurchaseHistory, 1)
end

local grantProductSuccess = pcall(DevProducts[receiptInfo.ProductId], player, keep)

if not grantProductSuccess then
return Enum.ProductPurchaseDecision.NotProcessedYet
end

table.insert(keep.Data.PurchaseHistory, receiptInfo.PurchaseId)

local saveSuccess = keep:Save():await()

if not saveSuccess then
return Enum.ProductPurchaseDecision.NotProcessedYet
end

return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = processReceipt
end

return setProcessReceipt
-- Main.luau

local Players = game:GetService("Players")

local DataKeep = require(path_to_datakeep)
local DataTemplate = require(path_to_datatemplate)
local SetProcessReceipt = require(path_to_setprocessreceipt)

local keyPrefix = "Player_"

local loadedKeeps = {}

local keepStore = DataKeep.GetStore("PlayerData", DataTemplate, {}):expect()

local function onPlayerAdded(player: Player)
keepStore:LoadKeep(keyPrefix .. player.UserId):andThen(function(keep)
if keep == nil then
player:Kick("Session lock interrupted!")
end

keep:Reconcile()
keep:AddUserId(player.UserId) -- help with GDPR requests

keep.Releasing:Connect(function(state) -- don't have to clean up, it cleans up internally
state:andThen(function()
print(`{player.Name}'s Keep has been released!`)

player:Kick("Session released!")
loadedKeeps[player] = nil
end):catch(function(err)
warn(`{player.Name}'s Keep failed to release!`, err)
end)
end)

if not player:IsDescendantOf(Players) then
keep:Release()
return
end

loadedKeeps[player] = keep

print(`Loaded {player.Name}'s Keep!`)
end)
end

-- SetProcessReceipt() must be called before the onPlayerAdded(),
-- otherwise the player's existing receipts won't be processed.
SetProcessReceipt(keepStore, keyPrefix)

-- loop through already connected players in case they joined before DataKeep loaded
for _, player in Players:GetPlayers() do
task.spawn(onPlayerAdded, player)
end

Players.PlayerAdded:Connect(onPlayerAdded)

Players.PlayerRemoving:Connect(function(player)
local keep = loadedKeeps[player]

if not keep then
return
end

keep:Release()
end)