New Merch Booth Developer Module

This is a great resource to help developers monetize their experiences! I actually added it into one of my experiences already to try it out!

A few thoughts on this module:

Feature Request: Add a function to open the catalog to a subset of items
It would be helpful if the catalog could be opened to a subset of the items available in the catalog. This idea essentially combines the concept of the openItemView function with the catalog filtering functionality.

Possible use cases:

  • When a player interacts with an ATM, open the catalog to the “Cash” developer products rather than to all the developer products / the entire catalog.
  • When a player inspects a mannequin’s outfit, open the catalog to only the items displayed on the mannequin.

Two implementation ideas for how the items to display could be specified:

  1. Pass an itemId array to the function.
  2. Allow the developer to tag an itemId with a “category”. To open to a subset of items, the developer would call the function with the desired category name. Note: If this option is implemented, it would be beneficial to allow an itemId to be present in multiple categories at the same time (e.g. the developer may wish to use the same item in multiple outfits).

Bug: The removeItem function does not remove the specified item
The removeItem function does not actually remove the specified item from the catalog. After digging into the code a little bit, I found that the client-side implementation was incomplete (the code needed in the setEnabled module was missing). Here is the setEnabled module code with the finished implementation:

local LocalizationService = game:GetService("LocalizationService")
local UserInputService = game:GetService("UserInputService")
local MarketplaceService = game:GetService("MarketplaceService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local MerchBooth = script:FindFirstAncestor("MerchBooth")

local Roact = require(MerchBooth.Packages["Roact_1.4.3"])
local RoactRodux = require(MerchBooth.Packages["RoactRodux_0.5.0"])
local t = require(MerchBooth.Packages["t_3.0.0"])
local App = require(MerchBooth.Components.App)
local getCurrentDevice = require(MerchBooth.Modules.getCurrentDevice)
local uiStatesEvents = require(MerchBooth.Api.uiStatesEvents)
local itemEvents = require(MerchBooth.Api.itemEvents)
local reducer = require(MerchBooth.Reducers.reducer)
local setMerchBoothVisible = require(MerchBooth.Actions.setMerchBoothVisible)
local setItemInfo = require(MerchBooth.Actions.setItemInfo)
local removeItemInfo = require(MerchBooth.Actions.removeItemInfo)
local setMerchBoothEnabled = require(MerchBooth.Actions.setMerchBoothEnabled)
local updateItemInfo = require(MerchBooth.Actions.updateItemInfo)
local addProximityButton = require(MerchBooth.Actions.addProximityButton)
local removeProximityButton = require(MerchBooth.Actions.removeProximityButton)
local setDevice = require(MerchBooth.Actions.setDevice)

local initialLoadData = MerchBooth.Remote.InitialLoadData :: RemoteEvent

return function(store: any)
	local LocalPlayer = Players.LocalPlayer

	local app = Roact.createElement(RoactRodux.StoreProvider, {
		store = store,
	}, {
		Roact.createElement(App),
	})

	local mountedTree
	local connections = {}

	local function onItemInfoReceived(id: string, info: table)
		store:dispatch(setItemInfo(id, info))

		local isOwned = false
		if info.productType == Enum.InfoType.Asset then
			isOwned = MarketplaceService:PlayerOwnsAsset(LocalPlayer, id) -- yields
		elseif info.productType == Enum.InfoType.GamePass then
			isOwned = MarketplaceService:UserOwnsGamePassAsync(LocalPlayer.UserId, id)
		end
		store:dispatch(updateItemInfo(id, {
			isOwned = isOwned,
		}))

		-- Titles and descriptions are in EN since they come from the server. If locale is non-EN, they need to be
		-- fetched again in order to be translated
		if LocalizationService.RobloxLocaleId ~= "en-us" then
			task.spawn(function()
				local updatedInfo = MarketplaceService:GetProductInfo(tonumber(id), Enum.InfoType.Asset) -- yields
				store:dispatch(updateItemInfo(id, {
					title = updatedInfo.Name,
					description = updatedInfo.Description,
				}))
			end)
		end
	end

	local function setCurrentDevice()
		local device = getCurrentDevice(UserInputService:GetLastInputType())
		store:dispatch(setDevice(device))
	end

	--[=[
		Sets whether the entire MerchBooth is enabled (catalog view + item details + button + proximity buttons) or not.

		```lua
		local ReplicatedStorage = game:GetService("ReplicatedStorage")

		local MerchBooth = require(ReplicatedStorage:WaitForChild("MerchBooth"))

		MerchBooth.setEnabled(true)
		```

		@within MerchBooth
		@client
	]=]
	local function setEnabled(
		isEnabled: boolean,
		setItemInfoRemote: RemoteEvent?,
		removeItemInfoRemote: RemoteEvent?,
		addProximityButtonRemote: RemoteEvent?,
		removeProximityButtonRemote: RemoteEvent?,
		playParticleEmitterRemote: RemoteEvent?
	)
		assert(RunService:IsClient(), "MerchBooth.setEnabled must be called on the client")
		assert(t.boolean(isEnabled), "Bad argument #1 to MerchBooth.setEnabled: expected a boolean")

		store:dispatch(setMerchBoothEnabled(isEnabled))

		if isEnabled then
			connections.uiStateEvents = uiStatesEvents.connect(store)

			connections.itemEvents = itemEvents.connect(store, function(state: reducer.State)
				return state.server.itemInfo
			end)

			-- Mount MerchBoothUI to PlayerGui
			local playerGui = LocalPlayer:WaitForChild("PlayerGui")
			mountedTree = Roact.mount(app, playerGui)

			-- Setting up connections
			setItemInfoRemote = setItemInfoRemote or MerchBooth.Remote.SetItemInfo
			removeItemInfoRemote = removeItemInfoRemote or MerchBooth.Remote.RemoveItemInfo
			addProximityButtonRemote = addProximityButtonRemote or MerchBooth.Remote.AddProximityButton
			removeProximityButtonRemote = removeProximityButtonRemote or MerchBooth.Remote.RemoveProximityButton
			playParticleEmitterRemote = playParticleEmitterRemote or MerchBooth.Remote.PlayParticleEmitter

			connections.setItemInfoConnection = setItemInfoRemote.OnClientEvent:Connect(function(action)
				onItemInfoReceived(action.itemId, action.info)
			end)

			connections.removeItemInfoConnection = removeItemInfoRemote.OnClientEvent:Connect(function(action)
				store:dispatch(removeItemInfo(action.itemId))
			end)

			connections.addProximityConnection = addProximityButtonRemote.OnClientEvent:Connect(function(id, adornee)
				store:dispatch(addProximityButton(id, adornee))
			end)

			connections.removeProximityConnection = removeProximityButtonRemote.OnClientEvent:Connect(function(adornee)
				store:dispatch(removeProximityButton(adornee))
			end)

			connections.initialLoadEventConnection = initialLoadData.OnClientEvent:Connect(
				function(itemInfo, proximityButtons)
					for id, info in pairs(itemInfo) do
						onItemInfoReceived(id, info)
					end
					for _, tuple in ipairs(proximityButtons) do
						store:dispatch(addProximityButton(tuple[2], tuple[1]))
					end
				end
			)

			connections.characterAddedConnection = LocalPlayer.CharacterAdded:Connect(function()
				store:dispatch(setMerchBoothVisible(false))
			end)

			connections.playParticleEmitterConnection = playParticleEmitterRemote.OnClientEvent:Connect(function()
				local state: reducer.State = store:getState()
				local emitter = state.config.particleEmitterTemplate:Clone()
				emitter.Parent = LocalPlayer.Character.Humanoid.RootPart
				emitter:Emit(15)

				task.wait()
				if emitter.Parent then
					emitter.Enabled = false
				end

				task.wait(4)
				if emitter.Parent then
					emitter:Destroy()
				end
			end)

			connections.lastInputTypeChangedConnection = UserInputService.LastInputTypeChanged:Connect(setCurrentDevice)

			setCurrentDevice()
		else
			-- Unmount MerchBoothUI from PlayerGui
			if mountedTree then
				store:dispatch(setMerchBoothVisible(false))
				store:flush()
				Roact.unmount(mountedTree)
				mountedTree = nil
			end

			-- Disconnecting connections
			for _, connection in pairs(connections) do
				if typeof(connection) == "table" then
					-- The event returned by uiStateEvents uses a lowercase syntax
					connection:disconnect()
				else
					connection:Disconnect()
				end
			end
		end
	end

	return setEnabled
end

7 Likes

this is ‘official’ though - as it’ll be used in many games people may think it is safe.

1 Like

I think that maybe more help at the script

1 Like

I don’t think you understood my reply. Roblox official (official library release, not official substitute for the catalog) or not, anyone can create a scam interface to trick players. This release isn’t opening doors to any new scams.

Any player who wants to scam with a fake catalog can do so and could have been doing it long before this was released. You’re putting blame on the wrong people, this module isn’t causing any more damage.

2 Likes

It’s not an update is a free model Roblox released.

2 Likes

Hi,
This module looks amazing and I have been able to add it to our new project. I would like to ask about the proximity prompt. I’m successfully adding products as in the tutorial, but if I modify the merchbooth.additemasync as indicated in Merch Booth | Roblox Creator Documentation , I get several errors:


An example code looks like this:


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local MerchBooth = require(ReplicatedStorage:WaitForChild("MerchBooth"))

local success, errorMessage = pcall(function()
	MerchBooth.addItemAsync(4819740796)
end)
if success then
	local item = workspace:FindFirstChild("Robox")
	if item then
		MerchBooth.addProximityButton(item, 4819740796)
	end
end
3 Likes

In fact, These errors correspond to the proximity trying to be added to a regular part, they need to be added to an adornee (Surface GUI element). I managed to add one. thanks

3 Likes

Maybe think not too script error and or I know let’s me get just see how to get much Fix bug

3 Likes

This seems very useful. I really like the UI, It looks very clean.

2 Likes

Is there a way to view the amount of revenue generated from using the merch booth module?

3 Likes

This is a very interesting idea, we can look into this!

4 Likes

Hello developer I need some help I wanna ask you something removed Roblox system from finally gets fixed that server not getting issues I am so happy that you know

2 Likes

Absolutely love this Developer Module! Love how you can easily add any of your custom Gamepasses and items to the shop!

One feature that we would love added, currently with the Proximity Prompts implementation in the Developer Module, you can only call MerchBooth.addProximityButton(item, assetID) only once per asset id, meaning you can only add one proxmimity prompt per assetId.

I’ve tested this and when you attempt to call to add more than one proximity prompt per assetId, it will sort of just choose the first part when it is added. It would be awesome if functionality could be added to allow us to use add multiple proximity prompts for the same assetId! (In our case, our team has multiple shops all over the map, and we can only use a proximity prompt per assetId, in only one shop :sweat_smile:)

Some other users may want the same functionality so we can sell our shop items in multiple locations in our experiences! Other than that I really love this resource, can’t wait to see what other Developer Modules come out in the future! :smile:

2 Likes

Hi, there is no specific discussion and feedback thread for this module like all the others in the private category.

1 Like

Hey! I know this has nothing to do with the current topic but I have a question on why my script is not working. It’s supposed to kick a player when you click on the part and leave a message of just “…”. When I try to click on the part nothing happens.
Here is the script I used:

Local player = game.players.localplayer or game.players.playeradded:wait()

Script.parent.mouse click:connect(function()
Player:kick( … )
End)

On the output it says
Workspace.IA.script:4: expected identifier when parsing got ‘…’
IA is the name of the part being clicked and the order of the parts is kinda like this for what is contained in IA (I’m not sure what the actual term is):

IA(part)
IA(sound)
Clickdetector (click detector)
Script (script)
Script (the script that is not working)
Weld

Please help because I believe this is the last thing I need but it’s been troubling trying to figure it out :”)

4 Likes

You need to make your own #help-and-feedback:scripting-support post, you cannot just go onto a topic and ask a random question. Especially when you know this is the wrong place for it.

But, I can help. Make sure you have a click detector in the part, and then insert a new regular script. Put this code inside of that new script and delete the old one:

script.Parent.ClickDetector.MouseClick:Connect(function(player)
   player:Kick("...")
end)

Your issue was the improper capitalization, messed-up functions, and just a whole slew of stuff frankly. But please, stay on topic in a topic and don’t ask a random question, I appreciate it.

3 Likes

Thank you so much, this helped a lot. And In terms with me asking random questions, I’m fairly new to this website and new to script so I’m really sorry about the random question but I didn’t know to how to make my own question thing. (Once again I’m unsure of the official terms) thank you for letting me know, though. :slight_smile:

5 Likes

Trying to add some clothing items from the Avatar Shop to the Merch Booth and get flooded with errors like this:

I got all of the IDs directly from the Avatar Shop the day before adding them to the Merch Booth. Many of the items added do show up, but any that don’t are due to the above error. Any ideas what is causing this?

2 Likes

Hey! Thanks for flagging this, happy to try to help you debug this. I see an error 429 in there, which means that you’re hitting the requests/seconds limit for the Catalog endpoints (that’s unfortunately a Roblox limitation rather than an error in the module itself). You can work around this by calling the merch booth’s addItemAsync less often. For instance, if you’re registering all your items in a loop, try adding a task.wait(1) in that loop to force the script to wait one second between each call to addItemAsync. Hope that helps!

2 Likes

Thanks! The 429 error makes sense. I’m using the default script provided by the Merch Booth Documentation page which doesn’t include a wait in the loop. I’ll add one for sure.

Any idea about all the other HTTP 400 (Bad request) errors? These are item IDs I got from the Avatar Shop just yesterday. I know sometimes items are deleted or made not public, but at the moment it seems like over half the items I’ve added are getting the 400 error.

Even after adding a wait in between all the addItemAsync calls most items result in a 400 error.

2 Likes