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:
- Pass an
itemId
array to the function. - 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 anitemId
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