This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,41 @@
--[[
Connects to GuiService's browser close callback to retry purchase after upsell
]]
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local Roact = require(CorePackages.Roact)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local retryAfterUpsell = require(script.Parent.Parent.Parent.Thunks.retryAfterUpsell)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local function BrowserPurchaseFinishedConnector(props)
local onBrowserWindowClosed = props.onBrowserWindowClosed
--[[
CLILUACORE-309: The browser window closing is the ONLY
indication we have about when the user finished interacting
with the upsell flow on desktop.
]]
return Roact.createElement(ExternalEventConnection, {
event = GuiService.BrowserWindowClosed,
callback = onBrowserWindowClosed,
})
end
local function mapDispatchToProps(dispatch)
return {
onBrowserWindowClosed = function()
dispatch(retryAfterUpsell())
end,
}
end
BrowserPurchaseFinishedConnector = connectToStore(
nil,
mapDispatchToProps
)(BrowserPurchaseFinishedConnector)
return BrowserPurchaseFinishedConnector
@@ -0,0 +1,35 @@
--[[
Connects relevant Roblox engine events to the rodux store
]]
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local UpsellFlow = require(script.Parent.Parent.Parent.UpsellFlow)
local MarketplaceServiceEventConnector = require(script.Parent.MarketplaceServiceEventConnector)
local GamepadInputConnector = require(script.Parent.GamepadInputConnector)
local BrowserPurchaseFinishedConnector = require(script.Parent.BrowserPurchaseFinishedConnector)
local NativePurchaseFinishedConnector = require(script.Parent.NativePurchaseFinishedConnector)
local getUpsellFlow = require(script.Parent.Parent.Parent.NativeUpsell.getUpsellFlow)
local function EventConnections()
local upsellConnector
local upsellFlow = getUpsellFlow(UserInputService:GetPlatform())
if upsellFlow == UpsellFlow.Web then
upsellConnector = Roact.createElement(BrowserPurchaseFinishedConnector)
elseif upsellFlow == UpsellFlow.Mobile then
upsellConnector = Roact.createElement(NativePurchaseFinishedConnector)
end
return Roact.createElement("Folder", {}, {
MarketPlaceServiceEventConnector = Roact.createElement(MarketplaceServiceEventConnector),
GamepadInputConnector = Roact.createElement(GamepadInputConnector),
UpsellFinishedConnector = upsellConnector,
})
end
return EventConnections
@@ -0,0 +1,52 @@
--[[
A component that establishes a connection to a Roblox event when it is rendered.
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local ExternalEventConnection = Roact.Component:extend("ExternalEventConnection")
function ExternalEventConnection:init()
self.connection = nil
end
--[[
Render the child component so that ExternalEventConnections can be nested like so:
Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputBegan,
callback = inputBeganCallback,
}, {
Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputEnded,
callback = inputChangedCallback,
})
})
]]
function ExternalEventConnection:render()
return Roact.oneChild(self.props[Roact.Children])
end
function ExternalEventConnection:didMount()
local event = self.props.event
local callback = self.props.callback
self.connection = event:Connect(callback)
end
function ExternalEventConnection:didUpdate(oldProps)
if self.props.event ~= oldProps.event or self.props.callback ~= oldProps.callback then
self.connection:Disconnect()
self.connection = self.props.event:Connect(self.props.callback)
end
end
function ExternalEventConnection:willUnmount()
self.connection:Disconnect()
self.connection = nil
end
return ExternalEventConnection
@@ -0,0 +1,100 @@
--[[
Sets whether or not gamepad buttons should be shown, based on recently
received inputs
]]
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local SetGamepadEnabled = require(script.Parent.Parent.Parent.Actions.SetGamepadEnabled)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local gamepadInputs = {
[Enum.UserInputType.Gamepad1] = true,
[Enum.UserInputType.Gamepad2] = true,
[Enum.UserInputType.Gamepad3] = true,
[Enum.UserInputType.Gamepad4] = true,
}
local thumbstickInputs = {
[Enum.KeyCode.Thumbstick1] = true,
[Enum.KeyCode.Thumbstick2] = true,
}
--[[
Returns whether or not input changed, and what its new value is
UserInputService.InputChanged event fires very frequently when moving
the mouse or thumbsticks: we try to early-out if nothing changed so that
we don't send hundreds of dispatches through the store
]]
local function didInputChange(isGamepadEnabled, inputObject)
local newEnabledStatus = nil
if gamepadInputs[inputObject.UserInputType] then
if thumbstickInputs[inputObject.KeyCode] then
local position = inputObject.Position
if math.abs(position.X) > 0.1 or math.abs(position.Z) > 0.1 or math.abs(position.Y) > 0.1 then
newEnabledStatus = true
end
else
newEnabledStatus = true
end
else
newEnabledStatus = false
end
if newEnabledStatus ~= nil and newEnabledStatus ~= isGamepadEnabled then
return true, newEnabledStatus
end
return false, isGamepadEnabled
end
local function GamepadInputConnector(props)
local gamepadEnabled = props.gamepadEnabled
local inputChanged = props.inputChanged
local dispatchOnChange = function (inputObject)
local didChange, newEnabledStatus = didInputChange(gamepadEnabled, inputObject)
if didChange then
inputChanged(newEnabledStatus)
end
end
return Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputChanged,
callback = dispatchOnChange,
}, {
Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputBegan,
callback = dispatchOnChange,
})
})
end
local function mapStateToProps(state)
return {
gamepadEnabled = state.gamepadEnabled,
}
end
local function mapDispatchToProps(dispatch)
return {
inputChanged = function(newEnabledStatus)
dispatch(SetGamepadEnabled(newEnabledStatus))
end,
}
end
GamepadInputConnector = connectToStore(
mapStateToProps,
mapDispatchToProps
)(GamepadInputConnector)
return GamepadInputConnector
@@ -0,0 +1,19 @@
--[[
LayoutValuesConsumer will extract the LayoutValues object
from context and pass it into the given render callback
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LayoutValuesKey = require(script.Parent.Parent.Parent.LayoutValuesKey)
local LayoutValuesConsumer = Roact.Component:extend("LayoutValuesConsumer")
function LayoutValuesConsumer:render()
local service = self._context[LayoutValuesKey]
return self.props.render(service)
end
return LayoutValuesConsumer
@@ -0,0 +1,24 @@
--[[
LayoutValuesProvider is a simple wrapper component that injects the
specified services into context
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LayoutValuesKey = require(script.Parent.Parent.Parent.LayoutValuesKey)
local LayoutValuesProvider = Roact.Component:extend("LayoutValuesProvider")
function LayoutValuesProvider:init(props)
assert(type(props.layoutValues) == "table", "Expected required prop 'layoutValues' to be a table")
assert(type(props.render) == "function", "Expected prop 'render' to be a function")
self._context[LayoutValuesKey] = props.layoutValues
end
function LayoutValuesProvider:render()
return self.props.render()
end
return LayoutValuesProvider
@@ -0,0 +1,22 @@
--[[
LocalizationContextConsumer will extract the localization context
object from Roact context and provide it to the given render callback
Used for components that need to perform localization using this
project's LocalizationService
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LocalizationContextKey = require(script.Parent.Parent.Parent.LocalizationContextKey)
local LocalizationContextConsumer = Roact.Component:extend("LocalizationContextConsumer")
function LocalizationContextConsumer:render()
local localizationContext = self._context[LocalizationContextKey]
return self.props.render(localizationContext)
end
return LocalizationContextConsumer
@@ -0,0 +1,24 @@
--[[
LocalizationContextProvider is a simple wrapper component that injects the
specified services into context
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LocalizationContextKey = require(script.Parent.Parent.Parent.LocalizationContextKey)
local LocalizationContextProvider = Roact.Component:extend("LocalizationContextProvider")
function LocalizationContextProvider:init(props)
assert(props.localizationContext, "Missing required prop 'localizationContext'")
assert(props.render, "Missing required prop 'render'")
self._context[LocalizationContextKey] = props.localizationContext
end
function LocalizationContextProvider:render()
return self.props.render(LocalizationContextKey)
end
return LocalizationContextProvider
@@ -0,0 +1,77 @@
--[[
Connects Rodux store to external MarketplaceService events
]]
local CorePackages = game:GetService("CorePackages")
local MarketplaceService = game:GetService("MarketplaceService")
local Roact = require(CorePackages.Roact)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local PurchaseError = require(script.Parent.Parent.Parent.PurchaseError)
local ErrorOccurred = require(script.Parent.Parent.Parent.Actions.ErrorOccurred)
local completePurchase = require(script.Parent.Parent.Parent.Thunks.completePurchase)
local initiatePurchase = require(script.Parent.Parent.Parent.Thunks.initiatePurchase)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local function MarketplaceServiceEventConnector(props)
local onPurchaseRequest = props.onPurchaseRequest
local onProductPurchaseRequest = props.onProductPurchaseRequest
local onPurchaseGamePassRequest = props.onPurchaseGamePassRequest
local onServerPurchaseVerification = props.onServerPurchaseVerification
return Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptPurchaseRequested,
callback = onPurchaseRequest,
}, {
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptProductPurchaseRequested,
callback = onProductPurchaseRequest,
}, {
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptGamePassPurchaseRequested,
callback = onPurchaseGamePassRequest,
}, {
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.ServerPurchaseVerification,
callback = onServerPurchaseVerification,
})
})
})
})
end
MarketplaceServiceEventConnector = connectToStore(nil,
function(dispatch)
local function onPurchaseRequest(player, assetId, equipIfPurchased, currencyType)
dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased))
end
local function onProductPurchaseRequest(player, productId, equipIfPurchased, currencyType)
dispatch(initiatePurchase(productId, Enum.InfoType.Product, equipIfPurchased))
end
local function onPurchaseGamePassRequest(player, gamePassId)
dispatch(initiatePurchase(gamePassId, Enum.InfoType.GamePass, false))
end
-- Specific to purchasing dev products
local function onServerPurchaseVerification(serverResponseTable)
if not serverResponseTable then
dispatch(ErrorOccurred(PurchaseError.UnknownFailure))
else
dispatch(completePurchase())
end
end
return {
onPurchaseRequest = onPurchaseRequest,
onProductPurchaseRequest = onProductPurchaseRequest,
onPurchaseGamePassRequest = onPurchaseGamePassRequest,
onServerPurchaseVerification = onServerPurchaseVerification,
}
end)(MarketplaceServiceEventConnector)
return MarketplaceServiceEventConnector
@@ -0,0 +1,45 @@
--[[
Connects to MarketplaceService's callback for completing a native purchase, so that we can
retry after an upsell purchase was processed
]]
local CorePackages = game:GetService("CorePackages")
local MarketplaceService = game:GetService("MarketplaceService")
local Roact = require(CorePackages.Roact)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local ErrorOccurred = require(script.Parent.Parent.Parent.Actions.ErrorOccurred)
local PurchaseError = require(script.Parent.Parent.Parent.PurchaseError)
local retryAfterUpsell = require(script.Parent.Parent.Parent.Thunks.retryAfterUpsell)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local function NativePurchaseFinishedConnector(props)
local nativePurchaseFinished = props.nativePurchaseFinished
return Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.NativePurchaseFinished,
callback = nativePurchaseFinished,
})
end
local function mapDispatchToProps(dispatch)
return {
nativePurchaseFinished = function(player, productId, wasPurchased)
if wasPurchased then
dispatch(retryAfterUpsell())
else
dispatch(ErrorOccurred(PurchaseError.InvalidFunds))
end
end,
}
end
NativePurchaseFinishedConnector = connectToStore(
nil,
mapDispatchToProps
)(NativePurchaseFinishedConnector)
return NativePurchaseFinishedConnector
@@ -0,0 +1,22 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LocalizationService = require(script.Parent.Parent.Parent.Localization.LocalizationService)
local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer)
local function NumberLocalizer(props)
local number = props.number
local render = props.render
assert(typeof(number) == "number", "prop 'number' must be provided")
assert(typeof(render) == "function", "Render prop must be a function")
return Roact.createElement(LocalizationContextConsumer, {
render = function(localizationContext)
return render(LocalizationService.formatNumber(localizationContext, number))
end,
})
end
return NumberLocalizer
@@ -0,0 +1,23 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LocalizationService = require(script.Parent.Parent.Parent.Localization.LocalizationService)
local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer)
local function TextLocalizer(props)
local key = props.key
local params = props.params
local render = props.render
assert(typeof(key) == "string", "String key must be provided")
assert(typeof(render) == "function", "Render prop must be a function")
return Roact.createElement(LocalizationContextConsumer, {
render = function(localizationContext)
return render(LocalizationService.getString(localizationContext, key, params))
end,
})
end
return TextLocalizer
@@ -0,0 +1,21 @@
--[[
Helper for supplying the current Roblox Locale to the Roact
tree via context using the LocalizationContextProvider
]]
local CorePackages = game:GetService("CorePackages")
local LocalizationService = game:GetService("LocalizationService")
local Roact = require(CorePackages.Roact)
local LocalizationContextProvider = require(script.Parent.LocalizationContextProvider)
local getLocalizationContext = require(script.Parent.Parent.Parent.Localization.getLocalizationContext)
local function provideRobloxLocale(renderFunc)
return Roact.createElement(LocalizationContextProvider, {
localizationContext = getLocalizationContext(LocalizationService.RobloxLocaleId),
render = renderFunc
})
end
return provideRobloxLocale
@@ -0,0 +1,17 @@
--[[
Helpful wrapper around LayoutValuesConsumer to make it a
little less verbose to use
]]
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local LayoutValuesConsumer = require(script.Parent.LayoutValuesConsumer)
local function withLayoutValues(renderFunc)
return Roact.createElement(LayoutValuesConsumer, {
render = renderFunc,
})
end
return withLayoutValues
@@ -0,0 +1,126 @@
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local Roact = require(CorePackages.Roact)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local LocalizationService = require(script.Parent.Parent.Parent.Localization.LocalizationService)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local UpsellFlow = require(script.Parent.Parent.Parent.UpsellFlow)
local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local getUpsellFlow = require(script.Parent.Parent.Parent.NativeUpsell.getUpsellFlow)
local isMockingPurchases = require(script.Parent.Parent.Parent.isMockingPurchases)
local PURCHASE_DETAILS_KEY = "CoreScripts.PurchasePrompt.PurchaseDetails.%s"
local function AdditionalDetailLabel(props)
return withLayoutValues(function(values)
local layoutOrder = props.layoutOrder
local messageKey = props.messageKey
local messageParams = props.messageParams
if messageKey == nil then
-- We return an empty frame to preserve UIListLayout spacing
return Roact.createElement("Frame", {
LayoutOrder = layoutOrder,
Size = values.Size.AdditonalDetailsLabel,
BackgroundTransparency = 1,
BorderSizePixel = 0,
})
end
return Roact.createElement(TextLocalizer, {
key = messageKey,
params = messageParams,
render = function(localizedText)
return Roact.createElement("TextLabel", {
Text = localizedText,
LayoutOrder = layoutOrder,
Size = values.Size.AdditonalDetailsLabel,
BackgroundTransparency = 1,
BorderSizePixel = 0,
TextColor3 = Color3.new(1, 1, 1),
Font = Enum.Font.SourceSans,
TextSize = values.TextSize.AdditonalDetails,
TextYAlignment = Enum.TextYAlignment.Top,
TextScaled = true,
TextWrapped = true,
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = values.TextSize.AdditonalDetails,
})
})
end,
})
end)
end
local function mapStateToProps(state)
local promptState = state.promptState
local messageKey = nil
local messageParams = nil
local price = state.productInfo.price
local balance = state.accountInfo.balance
if promptState == PromptState.PromptPurchase then
if price == 0 then
messageKey = PURCHASE_DETAILS_KEY:format("BalanceUnaffected")
elseif isMockingPurchases() then
messageKey = PURCHASE_DETAILS_KEY:format("MockPurchase")
else
messageKey = PURCHASE_DETAILS_KEY:format("BalanceFuture")
messageParams = {
BALANCE_FUTURE = LocalizationService.numberParam(balance - price),
}
end
elseif promptState == PromptState.RobuxUpsell then
local upsellFlow = getUpsellFlow(UserInputService:GetPlatform())
if upsellFlow ~= UpsellFlow.Web then
local upsellRobux = state.nativeUpsell.robuxPurchaseAmount
local amountNeeded = price - balance
local amountRemaining = upsellRobux - amountNeeded
messageKey = PURCHASE_DETAILS_KEY:format("RemainingAfterUpsell")
messageParams = {
REMAINING_ROBUX = LocalizationService.numberParam(amountRemaining),
}
end
elseif promptState == PromptState.BuildersClubUpsell then
local bcLevelRequired = state.productInfo.bcLevelRequired
messageKey = PURCHASE_DETAILS_KEY:format("InvalidBuildersClub")
messageParams = {
BC_LEVEL = LocalizationService.nestedKeyParam(
LocalizationService.getBuildersClubLevelKey(bcLevelRequired)
)
}
elseif promptState == PromptState.PurchaseComplete then
if isMockingPurchases() then
messageKey = PURCHASE_DETAILS_KEY:format("MockPurchaseComplete")
else
messageKey = PURCHASE_DETAILS_KEY:format("BalanceNow")
messageParams = {
BALANCE_NOW = LocalizationService.numberParam(balance - price),
}
end
end
return {
messageKey = messageKey,
messageParams = messageParams,
}
end
AdditionalDetailLabel = connectToStore(
mapStateToProps
)(AdditionalDetailLabel)
return AdditionalDetailLabel
@@ -0,0 +1,33 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local AdditionalDetailLabel = require(script.Parent.AdditionalDetailLabel)
AdditionalDetailLabel = AdditionalDetailLabel.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(AdditionalDetailLabel, {
layoutOrder = 1,
messageKey = "test",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors when showing no text", function()
local emptyMessageElement = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(AdditionalDetailLabel, {
layoutOrder = 1,
})
})
local instance = Roact.mount(emptyMessageElement)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,37 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local GRAY = Color3.fromRGB(184, 184, 184)
local BLUE = Color3.fromRGB(0, 162, 255)
local function Dot(props)
local layoutOrder = props.layoutOrder
local time = props.time
local size = 0.8
local color = GRAY
if time >= layoutOrder -1 and time <= layoutOrder then
local animationProgress = math.sin(math.pi * (time % 1))
size = size + (1 - size) * animationProgress
color = GRAY:lerp(BLUE, animationProgress)
end
return Roact.createElement("Frame", {
Size = UDim2.new(1/3, 0, 1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
}, {
Dot = Roact.createElement("Frame", {
Size = UDim2.new(0.7, 0, size, 0),
SizeConstraint = Enum.SizeConstraint.RelativeYY,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
BackgroundColor3 = color,
BorderSizePixel = 0,
})
})
end
return Dot
@@ -0,0 +1,16 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local AnimatedDot = require(script.Parent.AnimatedDot)
it("should create and destroy without errors", function()
local element = Roact.createElement(AnimatedDot, {
time = 1,
layoutOrder = 1,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,81 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local join = require(script.Parent.Parent.Parent.join)
local AutoResizeList = Roact.Component:extend("AutoResizeList")
function AutoResizeList:init()
self.containerRef = Roact.createRef()
self.listRef = Roact.createRef()
self.contentSizeCallback = function()
if self.listRef.current and self.containerRef.current then
self:resizeContainer()
end
end
end
function AutoResizeList:didMount()
self:resizeContainer()
end
function AutoResizeList:resizeContainer()
self.containerRef.current.Size = UDim2.new(
0, self.listRef.current.AbsoluteContentSize.X,
0, self.listRef.current.AbsoluteContentSize.Y)
end
function AutoResizeList:render()
local layoutOrder = self.props.layoutOrder
local position = self.props.position
local anchorPoint = self.props.anchorPoint
local backgroundImage = self.props.backgroundImage
local sliceCenter = self.props.sliceCenter
local horizontalAlignment = self.props.horizontalAlignment
local verticalAlignment = self.props.verticalAlignment
local fillDirection = self.props.fillDirection
local listPadding = self.props.listPadding
local children = join(self.props[Roact.Children] or {}, {
["$ListLayout"] = Roact.createElement("UIListLayout", {
HorizontalAlignment = horizontalAlignment,
VerticalAlignment = verticalAlignment,
FillDirection = fillDirection,
Padding = listPadding,
SortOrder = Enum.SortOrder.LayoutOrder,
[Roact.Ref] = self.listRef,
[Roact.Change.AbsoluteContentSize] = self.contentSizeCallback,
})
})
if backgroundImage == nil then
return Roact.createElement("Frame", {
LayoutOrder = layoutOrder,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Position = position,
AnchorPoint = anchorPoint,
[Roact.Ref] = self.containerRef,
}, children)
else
local scaleType = (sliceCenter ~= nil) and Enum.ScaleType.Slice or Enum.ScaleType.Stretch
return Roact.createElement("ImageLabel", {
Image = backgroundImage,
LayoutOrder = layoutOrder,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScaleType = scaleType,
SliceCenter = sliceCenter,
Position = position,
AnchorPoint = anchorPoint,
[Roact.Ref] = self.containerRef,
}, children)
end
end
return AutoResizeList
@@ -0,0 +1,15 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local AutoResizeList = require(script.Parent.AutoResizeList)
it("should create and destroy without errors", function()
local element = Roact.createElement(AutoResizeList, {
layoutOrder = 1,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,30 @@
local CorePackages = game:GetService("CorePackages")
local TextService = game:GetService("TextService")
local Roact = require(CorePackages.Roact)
local join = require(script.Parent.Parent.Parent.join)
local function AutoSizedTextLabel(props)
assert(props.Text ~= nil, "Cannot autosize label with Text = nil")
local text = props.Text
local textSize = props.TextSize
local font = props.Font
local width = props.width
local totalTextSize
if text ~= nil then
totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(width, 10000))
else
totalTextSize = Vector2.new(0, 0)
end
local textLabelProps = join(props, {
width = Roact.None,
Size = UDim2.new(0, width, 0, totalTextSize.Y),
})
return Roact.createElement("TextLabel", textLabelProps)
end
return AutoSizedTextLabel
@@ -0,0 +1,30 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel)
it("should create and destroy without errors", function()
local element = Roact.createElement(AutoSizedTextLabel, {
Text = "Hello",
TextSize = 10,
Font = Enum.Font.SourceSans,
width = 100,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should throw when no text is provided", function()
local element = Roact.createElement(AutoSizedTextLabel, {
TextSize = 10,
Font = Enum.Font.SourceSans,
width = 100,
})
expect(function()
Roact.mount(element)
end).to.throw()
end)
end
@@ -0,0 +1,149 @@
local CorePackages = game:GetService("CorePackages")
local ContextActionService = game:GetService("ContextActionService")
local Roact = require(CorePackages.Roact)
local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local Button = Roact.PureComponent:extend("Button")
function Button:init()
local onClick = self.props.onClick
local imageDown = self.props.imageDown
local imageUp = self.props.imageUp
self.state = {
currentImage = imageUp
}
self.inputBegan = function()
self:setState({
currentImage = imageDown
})
end
self.inputEnded = function()
self:setState({
currentImage = imageUp
})
end
self.activated = function()
onClick()
self:setState({
currentImage = imageUp
})
end
end
function Button:didMount()
-- Some buttons need to support additional button bindings
local bindings = self.props.additionalBindings or {}
table.insert(bindings, self.props.gamepadButton)
ContextActionService:BindCoreAction(
self.props.stringKey,
self.props.onClick,
false,
unpack(bindings)
)
end
function Button:willUnmount()
ContextActionService:UnbindCoreAction(self.props.stringKey)
end
function Button:render()
assert(typeof(self.props.gamepadButton) == "EnumItem"
and self.props.gamepadButton.EnumType == Enum.KeyCode,
"Prop 'gamepadButton' is required and must be of type Enum.KeyCode")
return withLayoutValues(function(values)
local stringKey = self.props.stringKey
local font = self.props.font
local size = self.props.size
local position = self.props.position
local gamepadEnabled = self.props.gamepadEnabled
local gamepadButton = self.props.gamepadButton
local imageData = values.Image[self.state.currentImage]
local buttonContents = {
ButtonLabel = Roact.createElement(TextLocalizer, {
key = stringKey,
render = function(localizedText)
return Roact.createElement("TextLabel", {
Text = localizedText,
Font = font,
Size = UDim2.new(0.6, 0, 0.8, 0),
Position = UDim2.new(0.2, 0, 0.1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
TextColor3 = Color3.new(1, 1, 1),
TextSize = values.TextSize.Button,
TextScaled = true,
TextWrapped = false,
LayoutOrder = 2,
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = values.TextSize.Button,
}),
})
end,
})
}
if gamepadEnabled then
-- Using a frame allows the icon to be left-aligned and still
-- be in a size-constrained section of the overall button
buttonContents.ButtonIcon = Roact.createElement("Frame", {
Size = UDim2.new(0.2, 0, 0.8, 0),
Position = UDim2.new(0, 0, 0.1, 0),
BackgroundTransparency = 1,
BorderSizePixel = 0,
}, {
Icon = Roact.createElement("ImageLabel", {
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, values.Size.ButtonIconPadding, 0, values.Size.ButtonIconYOffset),
SizeConstraint = Enum.SizeConstraint.RelativeYY,
ScaleType = Enum.ScaleType.Fit,
Image = values.Image[gamepadButton.Name].Path,
BackgroundTransparency = 1,
BorderSizePixel = 0,
})
})
end
return Roact.createElement("ImageButton", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
AutoButtonColor = false,
Modal = true,
Size = size,
Position = position,
ScaleType = Enum.ScaleType.Slice,
Image = imageData.Path,
SliceCenter = imageData.SliceCenter,
[Roact.Event.InputBegan] = self.inputBegan,
[Roact.Event.InputEnded] = self.inputEnded,
[Roact.Event.Activated] = self.activated,
}, buttonContents)
end)
end
local function mapStateToProps(state)
return {
gamepadEnabled = state.gamepadEnabled
}
end
Button = connectToStore(mapStateToProps)(Button)
return Button
@@ -0,0 +1,48 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local Button = require(script.Parent.Button)
Button = Button.getUnconnected()
it("should create and destroy without errors with gamepad disabled", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(Button, {
gamepadEnabled = false,
stringKey = "testing123",
gamepadButton = Enum.KeyCode.ButtonA,
font = Enum.Font.SourceSans,
imageUp = "ButtonUp",
imageDown = "ButtonDown",
onClick = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with gamepad enabled", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(Button, {
gamepadEnabled = true,
stringKey = "testing123",
gamepadButton = Enum.KeyCode.ButtonA,
font = Enum.Font.SourceSans,
imageUp = "ButtonUp",
imageDown = "ButtonDown",
onClick = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,28 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Button = require(script.Parent.Button)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local function CancelButton(props)
return withLayoutValues(function(values)
local onClick = props.onClick
return Roact.createElement(Button, {
font = Enum.Font.SourceSans,
imageUp = "ButtonUpRight",
imageDown = "ButtonDownRight",
gamepadButton = Enum.KeyCode.ButtonB,
stringKey = "CoreScripts.PurchasePrompt.CancelPurchase.Cancel",
size = UDim2.new(0.5, 0, 1, 0),
position = UDim2.new(0.5, 0, 0, 0),
onClick = onClick,
})
end)
end
return CancelButton
@@ -0,0 +1,20 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local CancelButton = require(script.Parent.CancelButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(CancelButton, {
onClick = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,52 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local ClickScamDetector = require(script.Parent.Parent.Parent.ClickScamDetector)
local Button = require(script.Parent.Button)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local CONFIRM_BUTTON = Enum.KeyCode.ButtonA
local ConfirmButton = Roact.Component:extend("ConfirmButton")
function ConfirmButton:init()
local onClick = self.props.onClick
self.activated = function()
if self.clickScamDetector:isClickValid() then
onClick()
end
end
self.clickScamDetector = ClickScamDetector.new({
buttonInput = CONFIRM_BUTTON,
initialDelay = 0.5,
})
end
function ConfirmButton:willUnmount()
self.clickScamDetector:destroy()
end
function ConfirmButton:render()
return withLayoutValues(function(values)
local stringKey = self.props.stringKey
return Roact.createElement(Button, {
font = Enum.Font.SourceSansBold,
imageUp = "ButtonUpLeft",
imageDown = "ButtonDownLeft",
gamepadButton = CONFIRM_BUTTON,
stringKey = stringKey,
size = UDim2.new(0.5, 0, 1, 0),
onClick = self.activated,
})
end)
end
return ConfirmButton
@@ -0,0 +1,21 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local ConfirmButton = require(script.Parent.ConfirmButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(ConfirmButton, {
stringKey = "testing123",
onClick = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,61 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer)
local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel)
local PurchasingAnimation = require(script.Parent.PurchasingAnimation)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local function InProgressContents(props)
return withLayoutValues(function(values)
return Roact.createElement("ImageLabel", {
Size = UDim2.new(1, 0, 1, 0),
ScaleType = Enum.ScaleType.Slice,
Image = values.Image.InProgressBackground.Path,
SliceCenter = values.Image.InProgressBackground.SliceCenter,
BackgroundTransparency = 1,
BorderSizePixel = 0,
}, {
ListLayout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 20)
}),
PurchasingText = Roact.createElement(TextLocalizer, {
key = "CoreScripts.PurchasePrompt.Purchasing",
render = function(localizedText)
return Roact.createElement(AutoSizedTextLabel, {
width = values.Size.Dialog.X.Offset,
Text = localizedText,
BackgroundTransparency = 1,
BorderSizePixel = 0,
TextColor3 = Color3.new(1, 1, 1),
Font = Enum.Font.SourceSans,
TextSize = values.TextSize.Purchasing,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
TextScaled = true,
TextWrapped = true,
LayoutOrder = 1,
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = values.TextSize.Purchasing,
}),
})
end,
}),
PurchasingAnimation = Roact.createElement(PurchasingAnimation, {
layoutOrder = 2,
}),
})
end)
end
return InProgressContents
@@ -0,0 +1,20 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local InProgressContents = require(script.Parent.InProgressContents)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(InProgressContents, {
anchorPoint = Vector2.new(0, 0),
position = UDim2.new(0, 0, 0, 0),
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,59 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local function ItemPreviewImage(props)
return withLayoutValues(function(values)
local layoutOrder = props.layoutOrder
local promptState = props.promptState
local productImageUrl = props.productImageUrl
if promptState == PromptState.Error then
productImageUrl = values.Image.ErrorIcon.Path
end
return Roact.createElement("Frame", {
Size = values.Size.ItemPreviewContainerFrame,
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
}, {
ItemPreviewImageContainer = Roact.createElement("Frame", {
Size = values.Size.ItemPreviewWhiteFrame,
BackgroundTransparency = 0,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
BorderSizePixel = 0,
BackgroundColor3 = Color3.new(1, 1, 1),
}, {
ItemImage = Roact.createElement("ImageLabel", {
Size = values.Size.ItemPreview,
BackgroundTransparency = 1,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Image = productImageUrl,
})
})
})
end)
end
local function mapStateToProps(state)
return {
promptState = state.promptState,
productImageUrl = state.productInfo.imageUrl,
}
end
ItemPreviewImage = connectToStore(
mapStateToProps
)(ItemPreviewImage)
return ItemPreviewImage
@@ -0,0 +1,22 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local ItemPreviewImage = require(script.Parent.ItemPreviewImage)
ItemPreviewImage = ItemPreviewImage.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(ItemPreviewImage, {
layoutOrder = 1,
imageUrl = "",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,30 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Button = require(script.Parent.Button)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local function OkButton(props)
return withLayoutValues(function(values)
local onClick = props.onClick
return Roact.createElement(Button, {
font = Enum.Font.SourceSans,
imageUp = "ButtonUp",
imageDown = "ButtonDown",
gamepadButton = Enum.KeyCode.ButtonA,
additionalBindings = {
Enum.KeyCode.ButtonB,
},
stringKey = "CoreScripts.PurchasePrompt.Button.OK",
size = UDim2.new(1, 0, 0, values.Size.ButtonHeight-4),
onClick = onClick
})
end)
end
return OkButton
@@ -0,0 +1,20 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local OkButton = require(script.Parent.OkButton)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(OkButton, {
onClick = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,84 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local NumberLocalizer = require(script.Parent.Parent.Connection.NumberLocalizer)
local AutoResizeList = require(script.Parent.AutoResizeList)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local function Price(props)
return withLayoutValues(function(values)
local layoutOrder = props.layoutOrder
local showPrice = props.showPrice
local price = props.price
return showPrice and Roact.createElement(AutoResizeList, {
layoutOrder = layoutOrder,
horizontalAlignment = Enum.HorizontalAlignment.Left,
verticalAlignment = Enum.VerticalAlignment.Center,
fillDirection = Enum.FillDirection.Horizontal,
}, {
RobuxIconContainer = Roact.createElement("Frame", {
BorderSizePixel = 0,
BackgroundTransparency = 1,
Size = values.Size.RobuxIconContainerFrame,
LayoutOrder = 1,
}, {
RobuxIcon = Roact.createElement("ImageLabel", {
Size = values.Size.RobuxIcon,
BackgroundTransparency = 1,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
Image = values.Image.RobuxIcon.Path,
}),
}),
PriceTextLabel = Roact.createElement(NumberLocalizer, {
number = price,
render = function(localizedNumber)
return Roact.createElement("TextLabel", {
Text = localizedNumber,
LayoutOrder = 2,
Size = values.Size.PriceTextLabel,
BackgroundTransparency = 1,
BorderSizePixel = 0,
TextColor3 = Color3.fromRGB(2, 183, 87),
Font = Enum.Font.SourceSansBold,
TextSize = values.TextSize.Default,
TextXAlignment = Enum.TextXAlignment.Left,
TextScaled = true,
TextWrapped = true,
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = values.TextSize.Default,
})
})
end,
})
})
end)
end
local function mapStateToProps(state)
local promptState = state.promptState
local canPurchase = promptState ~= PromptState.CannotPurchase
and promptState ~= PromptState.Error
local free = state.productInfo.price == 0
return {
showPrice = canPurchase and not free,
price = state.productInfo.price,
}
end
Price = connectToStore(
mapStateToProps
)(Price)
return Price
@@ -0,0 +1,22 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local Price = require(script.Parent.Price)
Price = Price.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(Price, {
layoutOrder = 1,
imageUrl = "",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,135 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local PurchaseError = require(script.Parent.Parent.Parent.PurchaseError)
local LocalizationService = require(script.Parent.Parent.Parent.Localization.LocalizationService)
local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer)
local AutoSizedTextLabel = require(script.Parent.AutoSizedTextLabel)
local Price = require(script.Parent.Price)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local PURCHASE_MESSAGE_KEY = "CoreScripts.PurchasePrompt.PurchaseMessage.%s"
local function ProductDescription(props)
return withLayoutValues(function(values)
local layoutOrder = props.layoutOrder
local descriptionKey = props.descriptionKey
local descriptionParams = props.descriptionParams
return Roact.createElement("Frame", {
BorderSizePixel = 0,
Size = values.Size.ProductDescription,
BackgroundTransparency = 1,
LayoutOrder = layoutOrder,
}, {
ProductDescriptionPadding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, values.Size.ProductDescriptionPaddingTop),
}),
ProductDescriptionListLayout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
ProductDescriptionText = Roact.createElement(TextLocalizer, {
key = descriptionKey,
params = descriptionParams,
render = function(localizedText)
return Roact.createElement(AutoSizedTextLabel, {
width = values.Size.ProductDescription.X.Offset - values.Size.HorizontalPadding,
Text = localizedText,
BackgroundTransparency = 1,
BorderSizePixel = 0,
TextColor3 = Color3.new(1, 1, 1),
Font = Enum.Font.SourceSans,
TextSize = values.TextSize.ProductDescription,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextScaled = true,
TextWrapped = true,
LayoutOrder = 1,
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = values.TextSize.ProductDescription,
}),
})
end,
}),
Price = Roact.createElement(Price, {
layoutOrder = 2,
}),
})
end)
end
local function mapStateToProps(state)
local promptState = state.promptState
local isFree = state.productInfo.price == 0
local descriptionKey
local descriptionParams
if promptState == PromptState.PurchaseComplete then
descriptionKey = PURCHASE_MESSAGE_KEY:format("Succeeded")
descriptionParams = {
ITEM_NAME = state.productInfo.name,
NEEDED_AMOUNT = LocalizationService.numberParam(
state.productInfo.price - state.accountInfo.balance
),
ASSET_TYPE = LocalizationService.nestedKeyParam(
LocalizationService.getAssetTypeKey(state.productInfo.assetTypeId)
),
}
elseif promptState == PromptState.RobuxUpsell then
descriptionKey = PURCHASE_MESSAGE_KEY:format("NeedMoreRobux")
descriptionParams = {
ITEM_NAME = state.productInfo.name,
NEEDED_AMOUNT = LocalizationService.numberParam(
state.productInfo.price - state.accountInfo.balance
),
ASSET_TYPE = LocalizationService.nestedKeyParam(
LocalizationService.getAssetTypeKey(state.productInfo.assetTypeId)
),
}
elseif promptState == PromptState.CannotPurchase or promptState == PromptState.Error then
descriptionKey = LocalizationService.getErrorKey(state.purchaseError)
if state.purchaseError == PurchaseError.UnknownFailure then
descriptionParams = {
ITEM_NAME = state.productInfo.name
}
end
elseif promptState ~= PromptState.Hidden then
if isFree then
descriptionKey = PURCHASE_MESSAGE_KEY:format("Free")
descriptionParams = {
ITEM_NAME = state.productInfo.name,
}
else
descriptionKey = PURCHASE_MESSAGE_KEY:format("Purchase")
descriptionParams = {
ASSET_TYPE = LocalizationService.nestedKeyParam(
LocalizationService.getAssetTypeKey(state.productInfo.assetTypeId)
),
ITEM_NAME = state.productInfo.name,
}
end
end
return {
descriptionKey = descriptionKey,
descriptionParams = descriptionParams,
}
end
ProductDescription = connectToStore(
mapStateToProps
)(ProductDescription)
return ProductDescription
@@ -0,0 +1,29 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local Reducer = require(script.Parent.Parent.Parent.Reducers.Reducer)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local ProductDescription = require(script.Parent.ProductDescription)
ProductDescription = ProductDescription.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, {
overrideStore = Rodux.Store.new(Reducer, {
productInfo = {
price = 10,
},
})
}, {
Roact.createElement(ProductDescription, {
descriptionKey = "CoreScripts.PurchasePrompt.PurchaseMessage.Purchase",
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,105 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local purchaseItem = require(script.Parent.Parent.Parent.Thunks.purchaseItem)
local launchRobuxUpsell = require(script.Parent.Parent.Parent.Thunks.launchRobuxUpsell)
local launchBuildersClubUpsell = require(script.Parent.Parent.Parent.Thunks.launchBuildersClubUpsell)
local ConfirmButton = require(script.Parent.ConfirmButton)
local CancelButton = require(script.Parent.CancelButton)
local OkButton = require(script.Parent.OkButton)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local CONFIRM_PURCHASE_KEY = "CoreScripts.PurchasePrompt.ConfirmPurchase.%s"
local PromptButtons = Roact.PureComponent:extend("PromptButtons")
function PromptButtons:render()
return withLayoutValues(function(values)
local layoutOrder = self.props.layoutOrder
local onClose = self.props.onClose
local promptState = self.props.promptState
local price = self.props.price
local onBuy = self.props.onBuy
local onRobuxUpsell = self.props.onRobuxUpsell
local onBuildersClubUpsell = self.props.onBuildersClubUpsell
local children
if promptState == PromptState.PurchaseComplete
or promptState == PromptState.CannotPurchase
or promptState == PromptState.Error
then
children = {
UIPadding = Roact.createElement("UIPadding", {
PaddingBottom = UDim.new(0, 4),
}),
OkButton = Roact.createElement(OkButton, {
onClick = onClose,
}),
}
else
local confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("BuyNow")
local leftButtonCallback = onBuy
if price == 0 then
confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("TakeFree")
elseif promptState == PromptState.RobuxUpsell then
confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("BuyRobux")
leftButtonCallback = onRobuxUpsell
elseif promptState == PromptState.BuildersClubUpsell then
confirmButtonStringKey = CONFIRM_PURCHASE_KEY:format("UpgradeBuildersClub")
leftButtonCallback = onBuildersClubUpsell
end
children = {
ConfirmButton = Roact.createElement(ConfirmButton, {
stringKey = confirmButtonStringKey,
onClick = leftButtonCallback,
}),
CancelButton = Roact.createElement(CancelButton, {
onClick = onClose,
}),
}
end
return Roact.createElement("Frame", {
LayoutOrder = layoutOrder,
BackgroundTransparency = 1,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, values.Size.ButtonHeight)
}, children)
end)
end
local function mapStateToProps(state)
return {
promptState = state.promptState,
price = state.productInfo.price,
}
end
local function mapDispatchToProps(dispatch)
return {
onBuy = function()
dispatch(purchaseItem())
end,
onRobuxUpsell = function()
dispatch(launchRobuxUpsell())
end,
onBuildersClubUpsell = function()
dispatch(launchBuildersClubUpsell())
end,
}
end
PromptButtons = connectToStore(
mapStateToProps,
mapDispatchToProps
)(PromptButtons)
return PromptButtons
@@ -0,0 +1,42 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local PromptButtons = require(script.Parent.PromptButtons)
PromptButtons = PromptButtons.getUnconnected()
local function noop()
end
it("should create and destroy without errors with one button", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(PromptButtons, {
layoutOrder = 1,
onClose = noop,
promptState = PromptState.PurchaseComplete,
price = 1,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
it("should create and destroy without errors with two buttons", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(PromptButtons, {
layoutOrder = 1,
onClose = noop,
promptState = PromptState.PromptPurchase,
price = 1,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,55 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local AutoResizeList = require(script.Parent.AutoResizeList)
local ItemPreviewImage = require(script.Parent.ItemPreviewImage)
local ProductDescription = require(script.Parent.ProductDescription)
local PromptButtons = require(script.Parent.PromptButtons)
local AdditionalDetailLabel = require(script.Parent.AdditionalDetailLabel)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local function PromptContents(props)
return withLayoutValues(function(values)
local onClose = props.onClose
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BorderSizePixel = 0,
BackgroundTransparency = 1,
}, {
ListLayout = Roact.createElement("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
PromptBody = Roact.createElement(AutoResizeList, {
layoutOrder = 1,
backgroundImage = values.Image.PromptBackground.Path,
sliceCenter = values.Image.PromptBackground.SliceCenter,
fillDirection = Enum.FillDirection.Vertical,
}, {
ProductInfo = Roact.createElement(AutoResizeList, {
layoutOrder = 1,
fillDirection = Enum.FillDirection.Horizontal,
}, {
ItemPreviewImage = Roact.createElement(ItemPreviewImage, {
layoutOrder = 1,
}),
ProductDescription = Roact.createElement(ProductDescription, {
layoutOrder = 2,
})
}),
AdditionalDetails = Roact.createElement(AdditionalDetailLabel, {
layoutOrder = 2,
}),
}),
PromptButtons = Roact.createElement(PromptButtons, {
layoutOrder = 2,
onClose = onClose,
}),
})
end)
end
return PromptContents
@@ -0,0 +1,38 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local Reducer = require(script.Parent.Parent.Parent.Reducers.Reducer)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local PromptContents = require(script.Parent.PromptContents)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, {
promptState = PromptState.PromptPurchase,
overrideStore = Rodux.Store.new(Reducer, {
promptState = PromptState.PromptPurchase,
accountInfo = {
balance = 100,
},
productInfo = {
assetTypeId = 2, -- T-shirt
price = 10,
},
})
}, {
Roact.createElement(PromptContents, {
layoutOrder = 1,
onClose = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,114 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Otter = require(CorePackages.Otter)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local HidePrompt = require(script.Parent.Parent.Parent.Actions.HidePrompt)
local PromptContents = require(script.Parent.PromptContents)
local InProgressContents = require(script.Parent.InProgressContents)
local connectToStore = require(script.Parent.Parent.Parent.connectToStore)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local PurchasePrompt = Roact.Component:extend(script.Name)
local SPRING_CONFIG = {
dampingRatio = 1,
frequency = 1.6,
}
function PurchasePrompt:init()
local setPromptHidden = self.props.setPromptHidden
self.motor = Otter.createSingleMotor(0)
self.motor:start()
self.ref = Roact.createRef()
self.motor:onStep(function(value)
local frame = self.ref.current
if frame then
frame.AnchorPoint = Vector2.new(0.5, 1 - 0.5 * value)
frame.Position = UDim2.new(0.5, 0, 0.5 * value, 0)
end
end)
self.onClose = function()
local onCompleteDisconnector
onCompleteDisconnector = self.motor:onComplete(function()
setPromptHidden()
onCompleteDisconnector()
self.motor:stop()
end)
self.motor:setGoal(Otter.spring(0, SPRING_CONFIG))
end
end
function PurchasePrompt:didUpdate(prevProps, prevState)
if prevProps.promptState ~= self.props.promptState then
local goal = self.props.promptState == PromptState.Hidden and 0 or 1
self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG))
if prevProps.promptState == PromptState.Hidden then
self.motor:start()
end
end
end
function PurchasePrompt:render()
return withLayoutValues(function(values)
local promptState = self.props.promptState
local contents
if promptState == PromptState.Hidden then
--[[
When the prompt is hidden, we'd rather not keep unused Roblox
instances for it around, so we don't render them
]]
contents = nil
elseif promptState == PromptState.PurchaseInProgress or promptState == PromptState.UpsellInProgress then
contents = Roact.createElement(InProgressContents)
else
contents = Roact.createElement(PromptContents, {
onClose = self.onClose,
})
end
return Roact.createElement("Frame", {
Size = values.Size.Dialog,
BorderSizePixel = 0,
BackgroundTransparency = 1,
[Roact.Ref] = self.ref
}, {
PromptContents = contents,
})
end)
end
local function mapStateToProps(state)
return {
promptState = state.promptState
}
end
local function mapDispatchToProps(dispatch)
return {
setPromptHidden = function()
dispatch(HidePrompt())
end
}
end
PurchasePrompt = connectToStore(
mapStateToProps,
mapDispatchToProps
)(PurchasePrompt)
return PurchasePrompt
@@ -0,0 +1,36 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local Reducer = require(script.Parent.Parent.Parent.Reducers.Reducer)
local PromptState = require(script.Parent.Parent.Parent.PromptState)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local PurchasePrompt = require(script.Parent.PurchasePrompt)
PurchasePrompt = PurchasePrompt.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, {
overrideStore = Rodux.Store.new(Reducer, {
promptState = PromptState.PromptPurchase,
accountInfo = {
balance = 100,
},
productInfo = {
assetTypeId = 2, -- T-shirt
price = 10,
},
})
}, {
Roact.createElement(PurchasePrompt, {
promptState = PromptState.PromptPurchase,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,68 @@
local CorePackages = game:GetService("CorePackages")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Roact = require(CorePackages.Roact)
local AnimatedDot = require(script.Parent.AnimatedDot)
local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local ANIMATION_SPEED_MULTIPLIER = 2.00
local PurchasingAnimation = Roact.Component:extend("PurchasingAnimation")
function PurchasingAnimation:init()
self.state = {
gameTime = 0,
}
self.onRenderStepped = function()
self:setState({
gameTime = Workspace.DistributedGameTime
})
end
end
function PurchasingAnimation:render()
return withLayoutValues(function(values)
local layoutOrder = self.props.layoutOrder
local gameTime = self.state.gameTime
local animationTime = (gameTime * ANIMATION_SPEED_MULTIPLIER) % 3
return Roact.createElement("Frame", {
Size = values.Size.PurchasingAnimation,
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
}, {
RenderSteppedConnection = Roact.createElement(ExternalEventConnection, {
event = RunService.RenderStepped,
callback = self.onRenderStepped,
}),
ListLayout = Roact.createElement("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
}),
AnimatedDot1 = Roact.createElement(AnimatedDot, {
layoutOrder = 1,
time = animationTime,
}),
AnimatedDot2 = Roact.createElement(AnimatedDot, {
layoutOrder = 2,
time = animationTime,
}),
AnimatedDot3 = Roact.createElement(AnimatedDot, {
layoutOrder = 3,
time = animationTime,
}),
})
end)
end
return PurchasingAnimation
@@ -0,0 +1,19 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local UnitTestContainer = require(script.Parent.Parent.Parent.Test.UnitTestContainer)
local PurchasingAnimation = require(script.Parent.PurchasingAnimation)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(PurchasingAnimation, {
layoutOrder = 1,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,73 @@
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local Rodux = require(CorePackages.Rodux)
local RoactRodux = require(CorePackages.RoactRodux)
local Thunk = require(script.Parent.Parent.Thunk)
local EventConnections = require(script.Parent.Connection.EventConnections)
local PurchasePrompt = require(script.Parent.Presentation.PurchasePrompt)
local LayoutValuesProvider = require(script.Parent.Connection.LayoutValuesProvider)
local provideRobloxLocale = require(script.Parent.Connection.provideRobloxLocale)
local Network = require(script.Parent.Parent.Services.Network)
local Analytics = require(script.Parent.Parent.Services.Analytics)
local PlatformInterface = require(script.Parent.Parent.Services.PlatformInterface)
local ExternalSettings = require(script.Parent.Parent.Services.ExternalSettings)
local LayoutValues = require(script.Parent.Parent.Services.LayoutValues)
local Reducer = require(script.Parent.Parent.Reducers.Reducer)
local preloadImageAssets = require(script.Parent.Parent.preloadImageAssets)
local PurchasePromptApp = Roact.Component:extend("PurchasePromptApp")
function PurchasePromptApp:init()
local initialState = {}
local network = Network.new()
local analytics = Analytics.new()
local platformInterface = PlatformInterface.new()
local externalSettings = ExternalSettings.new()
self.state = {
store = Rodux.Store.new(Reducer, initialState, {
Thunk.middleware({
[Network] = network,
[Analytics] = analytics,
[PlatformInterface] = platformInterface,
[ExternalSettings] = externalSettings,
}),
}),
layoutValues = LayoutValues.generate(externalSettings.isTenFootInterface()),
}
end
function PurchasePromptApp:didMount()
preloadImageAssets(self.state.layoutValues.Image)
end
function PurchasePromptApp:render()
return provideRobloxLocale(function()
return Roact.createElement(RoactRodux.StoreProvider, {
store = self.state.store,
}, {
PurchasePrompt = Roact.createElement(LayoutValuesProvider, {
layoutValues = self.state.layoutValues,
render = function()
return Roact.createElement("ScreenGui", {
AutoLocalize = false,
IgnoreGuiInset = true,
}, {
PurchasePromptUI = Roact.createElement(PurchasePrompt),
EventConnections = Roact.createElement(EventConnections),
})
end,
})
})
end)
end
return PurchasePromptApp
@@ -0,0 +1,13 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local PurchasePromptApp = require(script.Parent.PurchasePromptApp)
it("should create and destroy without errors", function()
local element = Roact.createElement(PurchasePromptApp)
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end