local Q={ProfileTemplate={Cash=0},Products={[97662780]=function(Q)Q.Data.Cash+=100 end;[97663121]=function(Q)Q.Data.Cash+=1000 end},PurchaseIdLog=50}local f=require(game.ServerScriptService.ProfileService)local R=game:GetService("\080\108\097\121\101\114\115")local j=game:GetService("\082\117\110\083\101\114\118\105\099\101")local D=game:GetService("\077\097\114\107\101\116\112\108\097\099\101\083\101\114\118\105\099\101")local U=f.GetProfileStore("\080\108\097\121\101\114\068\097\116\097",Q.ProfileTemplate)local k={}local function O(Q)local f=U:LoadProfileAsync("\080\108\097\121\101\114\095"..Q.UserId,"\070\111\114\099\101\076\111\097\100")if f~=nil then f:Reconcile()f:ListenToRelease(function()k[Q]=nil Q:Kick()end)if Q:IsDescendantOf(R)==true then k[Q]=f else f:Release()end else Q:Kick()end end function PurchaseIdCheckAsync(f,R,D)if f:IsActive()~=true then return Enum.ProductPurchaseDecision.NotProcessedYet else local U=f.MetaData local k=U.MetaTags.ProfilePurchaseIds if k==nil then k={}U.MetaTags.ProfilePurchaseIds=k end if table.find(k,R)==nil then while#k>=Q.PurchaseIdLog do table.remove(k,1)end table.insert(k,R)task.spawn(D)end local O=nil local function e()local Q=U.MetaTagsLatest.ProfilePurchaseIds if Q~=nil and table.find(Q,R)~=nil then O=Enum.ProductPurchaseDecision.PurchaseGranted end end e()local N=f:ListenToRelease(function()O=O or Enum.ProductPurchaseDecision.NotProcessedYet end)local S=f.MetaTagsUpdated:Connect(function()e()end)while O==nil do j.Heartbeat:Wait()end N:Disconnect()S:Disconnect()return O end end local function e(Q)local f=k[Q]while f==nil and Q:IsDescendantOf(R)==true do j.Heartbeat:Wait()f=k[Q]end return f end local function N(f,R)local j=k[f]local D=Q.Products[R]if D~=nil then D(j)else warn("\080\114\111\100\117\099\116\073\100 "..(tostring(R).." \104\097\115 \110\111\116 \098\101\101\110 \100\101\102\105\110\101\100 \105\110 \080\114\111\100\117\099\116\115 \116\097\098\108\101"))end end local function S(Q)local f=R:GetPlayerByUserId(Q.PlayerId)if f==nil then return Enum.ProductPurchaseDecision.NotProcessedYet end local j=e(f)if j~=nil then return PurchaseIdCheckAsync(j,Q.PurchaseId,function()N(f,Q.ProductId)end)else return Enum.ProductPurchaseDecision.NotProcessedYet end end for Q,f in ipairs(R:GetPlayers())do task.spawn(O,f)end D.ProcessReceipt=S R.PlayerAdded:Connect(O)R.PlayerRemoving:Connect(function(Q)local f=k[Q]if f~=nil then f:Release()end end)
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 RunService = game:GetService("RunService")
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,
		"ForceLoad"
	)
	if profile ~= nil then
		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 release_connection = profile:ListenToRelease(function()
			result = result or Enum.ProductPurchaseDecision.NotProcessedYet
		end)
		
		local meta_tags_connection = profile.MetaTagsUpdated:Connect(function()
			check_latest_meta_tags()
		end)
		
		while result == nil do
			RunService.Heartbeat:Wait()
		end
		
		release_connection:Disconnect()
		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
		RunService.Heartbeat: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)
