ProductService - A module for your game products!

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) and ProductService: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
  1. Insert ProductService into your game

  2. Place “Products” in ReplicatedStorage and “ProductService” somewhere where only the server has access to, like ServerScriptService or ServerStorage.
    image

  3. 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
  1. 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
  1. 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 :slight_smile:

32 Likes

How could we make it so a select few people own the gamepass w/o it being in their inventory?

1 Like

I’m sorry what? I didn’t get that well.

I can add a custom function for that :slight_smile:

1 Like

Yes, please :slight_smile: that would be so useful

1 Like

He’s asking how he can apply specific gamepass effects to players who don’t own the gamepass

1 Like

This module might be helpful for users who are still learning/just started to learn LuaU, however if you know some of the basic elements (like caching), you might not find it that helpful.

It’s still a cool contribution, and a cool source for newer users!

1 Like

si como yo xD, quiero intentar programar un gamepass que funcione y estoy mirando eventos relacionados con el gamepass y su compra. Todo sin ver tutoriales para poder avanzar, que me recomiendas?

1 Like

This line on ProductService might be buggy

local playerId = (playerInstance and playerInstance.UserId) or (type(user) == "number" and user) or (type(user) == "string" and Players:GetUserIdFromNameAsync(player)) or nil

It says unknown global player.
Should I be concerned?