INTRODUCTION
Working with MarketplaceService can be difficult and annoying because of itsunderlying issues:
Problems with MarketplaceService
-
In-game purchases of game passes are not cached. In other words, when a player purchases a gamepass,
MarketplaceService:UserOwnsGamePassAsync(playerId, passId)
will still return false. -
MarketplaceService:PromptGamePassPurchase(playerId, passId)
will prompt player to purchase even if already owned. -
Difficult to maintain game pass features and effects when player respawns (since
UserOwnsGamePassAsync
doesn’t cache properly). -
Confusing to handle ProcessReceipt Callback
Enum.ProductPurchaseDecision
-
Extremely messy and illegible code, especially if coder solely uses GamePass IDs and DevProduct IDs in their code in order to check ownership and apply effects.
I would like to introduce ProductService, a module I wrote as a workaround!
(Youtube video demonstration coming soon)
~
FEATURES
But Eric, why should I even use this when I could just code around MarketplaceService?
First of all, good luck. Second of all, ProductService uses MarketplaceService too in order to properly and accurately return data! It is extremely simple to use and provides unique functions and advantages that the MarketplaceService doesn’t!
-
Quickly and efficiently checks and caches player ownership of ALL listed game passes via pcall and coroutine upon joining the game.
-
ProductService:UserOwnsGamePass(user, pass)
returns ACCURATE ownership status. -
Applies game pass advantages upon character loaded and respawn (toggleable).
-
ProductService:PromptGamePassPurchase(player, pass)
prompts IF pass isn’t owned. -
Utility functions such as
ProductService:GetGamePassOwners(pass)
andProductService:GetOwnedGamePasses(player)
that can be useful to your game. -
Provides a simple way to give game pass, developer product, and premium effects to a player.
~
USING PRODUCT SERVICE
If you have read through the ProductService code and documentation and feel like you’ve got a pretty good idea on how to use it, don’t worry about opening this! If you need a step-by-step guide, open the steps below to continue!
Steps
-
Insert ProductService into your game
-
Place “Products” in ReplicatedStorage and “ProductService” somewhere where only the server has access to, like ServerScriptService or ServerStorage.
-
Open Products and change / remove / add any elements to fit your game’s use.
-- DEFAULT --
local Products = {
DevProducts = {
["25 Cash"] = 1006182985,
["Shield"] = 1006182986,
},
GamePasses = {
["VIP"] = 20057113,
["Tool"] = 20057114,
},
}
return Products
- Open ProductService. Change functions as necessary.
-- SAMPLE CODE --
-- CHANGE--
local Products = require(game:GetService("ReplicatedStorage").Products)
local APPLY_GAMEPASS_EFFECTS_ON_EVERY_SPAWN = true
--// GamePass Effects \\--
local GamePassFunctions = {}
GamePassFunctions["Tool"] = function(player)
local character = player.Character
local backpack = player:WaitForChild("Backpack")
if not character:FindFirstChild("Tool") then
if not backpack:FindFirstChild("Tool") then
game:GetService("ServerStorage").Tool:Clone().Parent = backpack
end
end
end
--// DevProduct Effects \\--
local DevProductFunctions = {}
DevProductFunctions["25 Cash"] = function(player)
if player.Parent then
if player:FindFirstChild("leaderstats") then
player.leaderstats.Cash.Value += 25
return true
end
end
end
--// Premium Effects \\--
applyPremiumEffects = function(player)
print(player.Name .. " is a premium member!")
end
- Insert a script in ServerScriptService that requires the ProductService, and everything will function for you!
-- SAMPLE CODE --
local ServerScriptService = game:GetService("ServerScriptService")
local ProductService = require(ServerScriptService.ProductService)
~
DOCUMENTATION
Source Code
local Players = game:GetService("Players")
local MarketplaceService = game:GetService("MarketplaceService")
local Signal = require(script.Signal)
local ProductService = {
GamePassPurchased = Signal.new(),
DevProductPurchased = Signal.new(),
PremiumPurchased = Signal.new()
}
-- Local copy of gamepasses and data (id, owners)
local GamePasses = {}
local TOTAL_GAME_PASSES = 0
for passName, passId in pairs(Products.GamePasses) do
GamePasses[passName] = {["Id"] = passId, ["Owners"] = {}}
TOTAL_GAME_PASSES += 1
end
--// Local Functions \\--
-- Returns devproduct name from id based on Products
local function getProductNameFromId(id)
for productName, productId in pairs(Products.DevProducts) do
if id == productId then
return productName
end
end
return nil
end
-- Returns gamepass name from id based on Products
local function getGamePassNameFromId(id)
for passName, passId in pairs(Products.GamePasses) do
if passId == id then
return passName
end
end
return nil
end
-- Returns player instance from name, userId, or instance
local function getPlayer(identification)
local idType = type(identification)
if idType== "string" then
return Players:FindFirstChild(identification)
elseif idType == "number" then
return Players:GetPlayerByUserId(identification)
elseif idType == "userdata" and identification.Parent == Players then
return identification
end
return nil
end
--// Modular Functions \\--
-- Checks if player owns GamePass
local CheckedPlayers = {}
local function yieldUntilCheck(player)
if not CheckedPlayers[player] then
repeat
task.wait()
until
CheckedPlayers[player] or not player.Parent
end
end
function ProductService:UserOwnsGamePass(user, pass)
local playerInstance = getPlayer(user)
local passName = (type(pass) == "number" and getGamePassNameFromId(pass)) or (type(pass) == "string" and Products.GamePasses[pass] and pass) or nil
-- Check if player owns a listed gamepass by searching through table of Owners
if playerInstance and passName then
yieldUntilCheck(playerInstance)
if playerInstance.Parent then
return table.find(GamePasses[passName].Owners, playerInstance) and true or false
end
-- Returns ownership using MarketplaceAsync
-- Status is cached, so it's inexpensive to call this function again
else
local playerId = (playerInstance and playerInstance.UserId) or (type(user) == "number" and user) or (type(user) == "string" and Players:GetUserIdFromNameAsync(player)) or nil
local passId = (type(pass) == "string" and Products.GamePasses[pass]) or (type(pass) == "number" and pass) or nil
if playerId and passId then
return MarketplaceService:UserOwnsGamePassAsync(playerId, passId)
end
end
return false
end
-- Prompts gamepass purchase only if user doesn't own the pass
function ProductService:PromptGamePassPurchase(user, pass)
local playerInstance = getPlayer(user)
if playerInstance then
local ownsPass = ProductService:UserOwnsGamePass(playerInstance, pass)
if not ownsPass then
local passId = (type(pass) == "string" and Products.GamePasses[pass]) or (type(pass) == "number" and pass) or nil
if passId and playerInstance.Parent then
MarketplaceService:PromptGamePassPurchase(playerInstance, passId)
end
end
end
end
-- Prompts product purchase
function ProductService:PromptProductPurchase(player, product)
local playerInstance = getPlayer(player)
if playerInstance then
if type(product) == "number" then
MarketplaceService:PromptProductPurchase(playerInstance, product)
elseif type(product) == "string" then
if Products.DevProducts[product] then
MarketplaceService:PromptProductPurchase(playerInstance, Products.DevProducts[product])
end
end
end
end
-- Prompts premium purchase if user doesn't own premium
function ProductService:PromptPremiumPurchase(player)
local playerInstance = getPlayer(player)
if playerInstance and not playerInstance.MembershipType == Enum.MembershipType.Premium then
MarketplaceService:PromptPremiumPurchase(playerInstance)
end
end
-- Returns table of all game passes owned by player
function ProductService:GetOwnedGamePasses(player)
local playerInstance = getPlayer(player)
if playerInstance then
yieldUntilCheck(playerInstance)
if playerInstance.Parent then
local ownedPasses = {}
for passName, passData in pairs(GamePasses) do
if table.find(passData.Owners, playerInstance) then
table.insert(ownedPasses, passName)
end
end
return ownedPasses
end
end
return {}
end
-- Returns table of game pass owners
function ProductService:GetGamePassOwners(pass)
local passName = (type(pass) == "number" and getGamePassNameFromId(pass)) or (type(pass) == "string" and pass) or nil
if passName and GamePasses[passName] then
return GamePasses[passName].Owners
end
return {}
end
--// Purchase Events \\--
-- GamePass purchased
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, purchased)
if player.Parent and purchased then
local passName = getGamePassNameFromId(passId)
if passName then
local passOwners = GamePasses[passName].Owners
if not table.find(passOwners, player) then
table.insert(passOwners, player)
GamePassFunctions[passName](player)
end
end
ProductService.GamePassPurchased:Fire(player, passName or "Unidentified GamePass", passId)
end
end)
-- DevProduct purchased
MarketplaceService.ProcessReceipt = function(receiptInfo)
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
if player.Parent then
local productName = getProductNameFromId(receiptInfo.ProductId)
if productName then
ProductService.DevProductPurchased:Fire(player, productName, receiptInfo)
local handler = DevProductFunctions[productName]
local success, result = pcall(handler, player, receiptInfo)
if success and result then
return Enum.ProductPurchaseDecision.PurchaseGranted
end
return Enum.ProductPurchaseDecision.NotProcessedYet
end
return Enum.ProductPurchaseDecision.PurchaseGranted
else
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end
-- Premium purchased
Players.PlayerMembershipChanged:Connect(function(player)
if player.MembershipType == Enum.MembershipType.Premium then
ProductService.PremiumPurchase:Fire(player)
end
end)
--// Player Initialization \\--
-- Apply gamepass effects each player respawn
local function characterAdded(character)
local player = Players:GetPlayerFromCharacter(character)
for passName, passData in pairs(GamePasses) do
if table.find(passData.Owners, player) then
coroutine.wrap(GamePassFunctions[passName])(player)
end
end
end
-- Check gamepass ownership of all gamepasses upon joinin
local function checkPassesOwnership(player)
local checkedPasses = 0
for passName, passData in pairs(GamePasses) do
coroutine.wrap(function()
local hasPass = false
local success, message = pcall(function()
hasPass = MarketplaceService:UserOwnsGamePassAsync(player.UserId, passData.Id)
end)
if success then
if hasPass then
table.insert(GamePasses[passName].Owners, player)
end
else
warn("Failed to check if " .. player.Name .. " owns gamepass: " .. passName)
end
checkedPasses += 1
end)()
end
-- Wait until all gamepasses has been checked
repeat
task.wait()
until
checkedPasses == TOTAL_GAME_PASSES or not player.Parent
-- Apply effects
if player.Parent then
CheckedPlayers[player] = true
if APPLY_GAMEPASS_EFFECTS_ON_EVERY_SPAWN then
characterAdded(player.Character or player.CharacterAdded:Wait())
player.CharacterAdded:Connect(characterAdded)
end
end
end
--// Player Connections \\--
-- Init all players
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(checkPassesOwnership)(player)
end
Players.PlayerAdded:Connect(checkPassesOwnership)
-- Reset checked status and ownership cache on leave
Players.PlayerRemoving:Connect(function(player)
CheckedPlayers[player] = nil
for passName, passData in pairs(GamePasses) do
local playerIndex = table.find(passData.Owners, player)
if playerIndex then
table.remove(passData.Owners, playerIndex)
end
end
end)
return ProductService
- Functions -
ProductService:UserOwnsGamePass(user, pass)
user: PlayerInstance, PlayerName, UserId
pass: GamePassId, GamePassName (listed in Products module)
ProductService:PromptGamePassPurchase(player, pass)
player: PlayerInstance, PlayerName, UserId
pass: GamePassId, GamePassName (listed in Products module)
ProductService:PromptProductPurchase(player, product)
player: PlayerInstance, PlayerName, UserId
product: ProductId, ProductName (listed in Products module)
ProductService:PromptPremiumPurchase(player)
player: PlayerInstance, PlayerName, UserId
ProductService:GetOwnedGamePasses(player)
player: PlayerInstance, PlayerName, UserId
returns table of names of gamepasses owned by user
ProductService:GetGamePassOwners(pass)
pass: GamePassId (must be in Products module), GamePassName (must be in Products module)
returns table of players who own gamepass
~
- Events -
ProductService.GamePassPurchased
returns PlayerInstance, GamePassName, GamePassId
ProductService.DevProductPurchased
returns PlayerInstance, ProductName, ReceiptInfo
ProductService.PremiumPurchased
returns PlayerInstance
Thank you for reading! All questions and feedback accepted