Developer Products: In‑Game Purchases

I am following my usual knowledge source of the Roblox Education site for learning how to make In-Game Purchases. The article in question is here:

However whether I make a test purchase in Studio I don’t seem to capture a success return from MarketplaceService.ProcessReceipt.
Thinking this was because I was in Studio, I made the purchases live in-game and it still failed to return a success when I tested.
I know the client side GUI portion works OK, as the live in-game purchase was deducted from my Robux total.
Can anybody see where I am going wrong:

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

-- Data store for tracking purchases that were successfully processed
local purchaseHistoryStore = DataStoreService:GetOrderedDataStore("DonationHistory")

-- Table setup containing product IDs and functions for handling purchases
local productFunctions = {}

-- ProductId 111111111111
productFunctions[111111111111] = function(receipt, player)
-- Logic/code for Thanking Player
	print(player.Name .. "  bought 111111111111")
end
-- ProductId 22222222222
productFunctions[22222222222] = function(receipt, player)
-- Logic/code for Thanking Player
	print(player.Name .. "  bought 22222222222")
end

local function processReceipt(receiptInfo)
	
	-- Determine if the product was already granted by checking the data store  
	local playerProductKey = receiptInfo.PlayerId .. "_" .. receiptInfo.PurchaseId
	local purchased = false
	local success, errorMessage = pcall(function()
		purchased = purchaseHistoryStore:GetAsync(playerProductKey)
	end)
	-- If purchase was recorded, the product was already granted
	if success and purchased then
		return Enum.ProductPurchaseDecision.PurchaseGranted
	elseif not success then
		error("Data store error:" .. errorMessage)
	end
	
	-- Find the player who made the purchase in the server
	local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if not player then
		-- The player probably left the game
		-- If they come back, the callback will be called again
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end
	
	-- Look up handler function from 'productFunctions' table above
	local handler = productFunctions[receiptInfo.ProductId]
	
	-- Call the handler function and catch any errors
	local success, result = pcall(handler, receiptInfo, player)
	if not success or not result then
		warn("Error occurred while processing a product purchase")
		print("ProductId:", receiptInfo.ProductId)
		print("Player:", player)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end
	
	-- Record transaction in data store so it isn't granted again
	local success, errorMessage = pcall(function()
		purchaseHistoryStore:SetAsync(playerProductKey, true)
	end)
	if not success then
		error("Cannot save purchase data: " .. errorMessage)
	end
	
	-- IMPORTANT: Tell Roblox that the game successfully handled the purchase
	return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = processReceipt

I am keen to get this to work as Premium Payouts are never going to earn me a cup of coffee each month.

3 Likes

Have you tried putting a print statement at the top of the processReceipt function to check if it runs at all?

Also have you tried using tonumber on the ProductId before indexing productFunctions (and/or making the keys strings)? It may be that you get a string value back from MarketplaceService instead of an integer.

2 Likes

So adding the following at the top of the function:

	print (receiptInfo)
	for i, v in pairs(receiptInfo) do
		print ("I: " .. i)
		print ("V type: " .. typeof(v))
		print ("V: " .. v)
		
	end

Gives a nice little table of the returned info:
table: 0x7a4a39908d4affc8
I: CurrencySpent
V type: number
V: 0
I: PurchaseId
V type: string
V: 2046550ca74e755c65353d4434e25915
I: ProductId
V type: number
V: 1104844206
I: PlaceIdWherePurchased
V type: number
V: 5271743600
I: CurrencyType
V type: EnumItem

So both ProductId and PlaceIdWherePurchased are integers

Okay good, so it’s running and returning the right things.

You don’t have a productFunction at the index 1104844206 so there’s nothing to run. Your pcall therefore can’t return success, so you should see the warning message.

You only have productFunctions at 111111111111 and 22222222222.

That was just bogus numbers I put in for posting here initially. The actual functions do exist with real ProductIDs which I have verified:

productFunctions[1104844206] = function(receipt, player)
-- Logic/code for Thanking Player
	print(player.Name .. " Donated 25 RBXP")
end
productFunctions[1104848933] = function(receipt, player)
-- Logic/code for Thanking Player
	print(player.Name .. " Donated 50 RBXP")
end
productFunctions[1104849360] = function(receipt, player)
-- Logic/code for Thanking Player
	print(player.Name .. " Donated 100 RBXP")
end

So for the table output above, it returned the first ProductID which is specified above.

It is actually failing at the following point:

	local success, result = pcall(handler, receiptInfo, player)
	if not success or not result then
		warn("Error occurred while processing a product purchase")
		print("ProductId:", receiptInfo.ProductId)
		print("Player:", player)
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

Try printing success and result immediately after the pcall.
print(success, result)
and see what the result is.

Do any warnings or any of the print statements inside that if block print to the console?

All of the warnings print fine in console. Adding the print(success, result) outputs:

 true nil
  01:18:23.078 - Error occurred while processing a product purchase
  ProductId: 1104848933
  Player: EmilyGaming9081

That to me implies that it can’t find the matching productFunctions[1104848933] earlier in the script, which doesn’t make sense to me at all.
^^ I used a different productID this time round, just to be really helpful :slight_smile:

There’s the issue. Your product functions don’t return anything, so result will always be nil if the function succeeded without error.

Change your if statement to be
if not success then

I suspect that you are right as usual. I will try tomorrow as it is really late now. Thanks for your help and I will let you know if it worked.

1 Like

Thank you @BanTech. Took me longer than expected to get around to testing as work interfered with life but this but it finally works and resultant returns are now saved into a Datastore.