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,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-fit-components"]["roact-fit-components"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_otter"]["otter"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact"]["roact"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_roact-rodux"]["roact-rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_rodux"]["rodux"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["UIBlox"]["UIBlox"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,15 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/purchase-prompt"
version = "0.1.6"
commit = "ad4ca756b1cee3e65122ca3459e2ae38668fcd54"
source = "git+https://github.com/roblox/purchasepromptscript-roact#master"
dependencies = [
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
"FitFrame roblox/roact-fit-components 1.2.5 url+https://github.com/roblox/roact-fit-components",
"Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter",
"Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact",
"RoactRodux roblox/roact-rodux 0.2.2 url+https://github.com/roblox/roact-rodux",
"Rodux roblox/rodux 1.0.0 url+https://github.com/roblox/rodux",
"UIBlox <patched> UIBlox a253d523 git+https://github.com/roblox/uiblox#master",
"t roblox/t 1.2.5 url+https://github.com/roblox/t",
]
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "accountInfo")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "bundleProductInfo")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name)
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "purchaseError")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "premiumInfo")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "productInfo")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "robuxProductId", "robuxPurchaseAmount")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name)
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "id", "equipIfPurchased", "isRobloxPurchase")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "id")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "id")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name)
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "id", "equipIfPurchased")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "id")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "key", "variation")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "enabled")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "id", "infoType", "equipIfPurchased")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "promptState")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "state")
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name)
@@ -0,0 +1,3 @@
local makeActionCreator = require(script.Parent.makeActionCreator)
return makeActionCreator(script.Name, "purchasingStartTime")
@@ -0,0 +1,42 @@
--[[
A helper function to define a named Rodux action creator.
Takes a name followed by a list of fields that should be
provided to the resulting action creator.
Returns an object with a name field that can also be called
to create an action. When called, it will validate its given
arguments against the expected set of arguments.
]]
local function makeActionCreator(name, ...)
local fields = {...}
assert(type(name) == "string",
"Bad argument #1 to makeActionCreator, expected string")
for i = 1, select("#", ...) do
assert(typeof(select(i, ...)) == "string",
"Bad argument to makeActionCreator, all arguments must be of type string")
end
return setmetatable({
name = name
}, {
__call = function(self, ...)
local result = {
type = name,
}
assert(select("#", ...) == #fields,
"Incorrect number of arguments provided to action creator " .. name)
for index, argName in ipairs(fields) do
result[argName] = select(index, ...)
end
return result
end
})
end
return makeActionCreator
@@ -0,0 +1,47 @@
return function()
local makeActionCreator = require(script.Parent.makeActionCreator)
describe("the generated action creator", function()
it("should throw if given an invalid arguments", function()
expect(function()
makeActionCreator(100)
end).to.throw()
expect(function()
makeActionCreator("Test", 12)
end).to.throw()
end)
it("should generate a callable table with a 'name' field", function()
local actionCreator = makeActionCreator("Action")
expect(type(actionCreator)).to.equal("table")
expect(actionCreator.name).to.equal("Action")
expect(actionCreator).never.to.throw()
end)
end)
describe("the action object generated by the action creator", function()
it("should expect a matching number of inputs", function()
local actionWithFields = makeActionCreator("AddItem", "id", "title")
expect(function()
actionWithFields(10, "Apple", "extra arg")
end).to.throw()
expect(function()
actionWithFields(10)
end).to.throw()
expect(function()
actionWithFields(10, "Orange")
end).never.to.throw()
end)
it("should correctly count trailing nils", function()
local actionWithFields = makeActionCreator("SetProperty", "value", "default")
expect(function()
actionWithFields("1", nil)
end).never.to.throw()
end)
end)
end
@@ -0,0 +1,42 @@
--[[
Connects to GuiService's browser close callback to retry purchase after upsell
]]
local Root = script.Parent.Parent.Parent
local GuiService = game:GetService("GuiService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local retryAfterUpsell = require(Root.Thunks.retryAfterUpsell)
local connectToStore = require(Root.connectToStore)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
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,38 @@
--[[
Connects relevant Roblox engine events to the rodux store
]]
local Root = script.Parent.Parent.Parent
local UserInputService = game:GetService("UserInputService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UpsellFlow = require(Root.Enums.UpsellFlow)
local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow)
local MarketplaceServiceEventConnector = require(script.Parent.MarketplaceServiceEventConnector)
local InputTypeManager = require(script.Parent.InputTypeManager)
local BrowserPurchaseFinishedConnector = require(script.Parent.BrowserPurchaseFinishedConnector)
local NativePurchaseFinishedConnector = require(script.Parent.NativePurchaseFinishedConnector)
local PlayerConnector = require(script.Parent.PlayerConnector)
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
local enableInputManager = UserInputService:GetPlatform() ~= Enum.Platform.XBoxOne
return Roact.createElement("Folder", {}, {
MarketPlaceServiceEventConnector = Roact.createElement(MarketplaceServiceEventConnector),
InputTypeManager = enableInputManager and Roact.createElement(InputTypeManager) or nil,
UpsellFinishedConnector = upsellConnector,
PlayerConnector = Roact.createElement(PlayerConnector),
})
end
return EventConnections
@@ -0,0 +1,53 @@
--[[
A component that establishes a connection to a Roblox event when it is rendered.
]]
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,105 @@
--[[
Sets whether or not gamepad buttons should be shown, based on recently
received inputs
]]
local Root = script.Parent.Parent.Parent
local CorePackages = game:GetService("CorePackages")
local UserInputService = game:GetService("UserInputService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local MouseIconOverrideService = require(CorePackages.InGameServices.MouseIconOverrideService)
local SetGamepadEnabled = require(Root.Actions.SetGamepadEnabled)
local PromptState = require(Root.Enums.PromptState)
local connectToStore = require(Root.connectToStore)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local CURSOR_OVERRIDE_KEY = "PurchasePromptOverrideKey"
local gamepadInputs = {
[Enum.UserInputType.Gamepad1] = true,
[Enum.UserInputType.Gamepad2] = true,
[Enum.UserInputType.Gamepad3] = true,
[Enum.UserInputType.Gamepad4] = true,
}
local InputTypeManager = Roact.Component:extend("InputTypeManager")
function InputTypeManager:init()
local setGamepadEnabled = self.props.setGamepadEnabled
self.dispatchOnChange = function(lastInputType)
local newEnabledStatus
if gamepadInputs[lastInputType] then
newEnabledStatus = true
else
newEnabledStatus = false
end
setGamepadEnabled(newEnabledStatus)
end
self.cursorOverridden = false
end
function InputTypeManager:render()
return Roact.createElement(ExternalEventConnection, {
event = UserInputService.LastInputTypeChanged,
callback = self.dispatchOnChange,
})
end
function InputTypeManager:didUpdate(prevProps, prevState)
local didShow = prevProps.promptState == PromptState.None
and self.props.promptState ~= PromptState.None
local didHide = prevProps.promptState ~= PromptState.None
and self.props.promptState == PromptState.None
local isShown = self.props.promptState ~= PromptState.None
local overrideStatus = self.props.gamepadEnabled
and Enum.OverrideMouseIconBehavior.ForceHide
or Enum.OverrideMouseIconBehavior.ForceShow
-- If we're already showing the prompt and the gamepad status changed
if isShown and prevProps.gamepadEnabled ~= self.props.gamepadEnabled then
if self.cursorOverridden then
MouseIconOverrideService.pop(CURSOR_OVERRIDE_KEY)
end
MouseIconOverrideService.push(CURSOR_OVERRIDE_KEY, overrideStatus)
self.cursorOverridden = true
-- If the purchase prompt goes from None to shown
elseif didShow then
MouseIconOverrideService.push(CURSOR_OVERRIDE_KEY, overrideStatus)
self.cursorOverridden = true
-- If the purchase prompt goes from shown to None
elseif didHide then
MouseIconOverrideService.pop(CURSOR_OVERRIDE_KEY)
self.cursorOverridden = false
end
end
local function mapStateToProps(state)
return {
promptState = state.promptState,
gamepadEnabled = state.gamepadEnabled,
}
end
local function mapDispatchToProps(dispatch)
return {
setGamepadEnabled = function(enabled)
dispatch(SetGamepadEnabled(enabled))
end,
}
end
InputTypeManager = connectToStore(
mapStateToProps,
mapDispatchToProps
)(InputTypeManager)
return InputTypeManager
@@ -0,0 +1,43 @@
--[[
LayoutValuesConsumer will extract the LayoutValues object
from context and pass it into the given render callback
]]
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LayoutValuesKey = require(Root.Symbols.LayoutValuesKey)
local LayoutValuesConsumer = Roact.Component:extend("LayoutValuesConsumer")
-- TODO(esauer): Add validation when adding t
-- local validateProps = t.strictInterface({
-- render = t.callback,
-- })
function LayoutValuesConsumer:init()
self.layoutValues = self._context[LayoutValuesKey]
self.state = {
layout = self.layoutValues.layout,
}
end
function LayoutValuesConsumer:render()
-- assert(validateProps(self.props))
return self.props.render(self.state.layout)
end
function LayoutValuesConsumer:didMount()
self.disconnectLayoutListener = self.layoutValues.signal:subscribe(function(newLayout)
self:setState({
layout = newLayout,
})
end)
end
function LayoutValuesConsumer:willUnmount()
self.disconnectLayoutListener()
end
return LayoutValuesConsumer
@@ -0,0 +1,65 @@
--[[
LayoutValuesProvider is a simple wrapper component that injects the
specified services into context
]]
local Root = script.Parent.Parent.Parent
local ContentProvider = game:GetService("ContentProvider")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LayoutValues = require(Root.Services.LayoutValues)
local LayoutValuesKey = require(Root.Symbols.LayoutValuesKey)
local connectToStore = require(Root.connectToStore)
local LayoutValuesProvider = Roact.Component:extend("LayoutValuesProvider")
function LayoutValuesProvider:init(props)
assert(type(props.isTenFootInterface) == "boolean", "Expected required prop 'isTenFootInterface' to be a boolean")
assert(type(props.render) == "function", "Expected prop 'render' to be a function")
self.layoutValues = LayoutValues.new(self.props.isTenFootInterface, false)
self._context[LayoutValuesKey] = self.layoutValues
end
function LayoutValuesProvider:didMount()
-- preload images
spawn(function()
local assets = {}
for _, image in pairs(self.layoutValues.layout.Image) do
local decal = Instance.new("Decal")
decal.Texture = image.Path
table.insert(assets, decal)
end
ContentProvider:PreloadAsync(assets)
for _,asset in pairs(assets) do
asset:Destroy()
end
end)
end
function LayoutValuesProvider:render()
return self.props.render()
end
function LayoutValuesProvider:didUpdate(previousProps)
if self.props.isTenFootInterface ~= previousProps.isTenFootInterface then
self.layoutValues:update(self.props.isTenFootInterface)
end
end
local function mapStateToProps(state)
return {
abVariations = state.abVariations,
}
end
LayoutValuesProvider = connectToStore(
mapStateToProps
)(LayoutValuesProvider)
return LayoutValuesProvider
@@ -0,0 +1,23 @@
--[[
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 Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LocalizationContextKey = require(Root.Symbols.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,25 @@
--[[
LocalizationContextProvider is a simple wrapper component that injects the
specified services into context
]]
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LocalizationContextKey = require(Root.Symbols.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,140 @@
--[[
Connects Rodux store to external MarketplaceService events
]]
local Root = script.Parent.Parent.Parent
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local ErrorOccurred = require(Root.Actions.ErrorOccurred)
local PurchaseError = require(Root.Enums.PurchaseError)
local completePurchase = require(Root.Thunks.completePurchase)
local initiatePurchase = require(Root.Thunks.initiatePurchase)
local initiateBundlePurchase = require(Root.Thunks.initiateBundlePurchase)
local initiatePremiumPurchase = require(Root.Thunks.initiatePremiumPurchase)
local initiateSubscriptionPurchase = require(Root.Thunks.initiateSubscriptionPurchase)
local connectToStore = require(Root.connectToStore)
local GetFFlagPromptRobloxPurchaseEnabled = require(Root.Flags.GetFFlagPromptRobloxPurchaseEnabled)
local GetFFlagDeveloperSubscriptionsEnabled = require(Root.Flags.GetFFlagDeveloperSubscriptionsEnabled)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local function MarketplaceServiceEventConnector(props)
local onPurchaseRequest = props.onPurchaseRequest
local onProductPurchaseRequest = props.onProductPurchaseRequest
local onPurchaseGamePassRequest = props.onPurchaseGamePassRequest
local onServerPurchaseVerification = props.onServerPurchaseVerification
local onBundlePurchaseRequest = props.onBundlePurchaseRequest
local onPremiumPurchaseRequest = props.onPremiumPurchaseRequest
local onRobloxPurchaseRequest = props.onRobloxPurchaseRequest
local onSubscriptionPurchaseRequest = props.onSubscriptionPurchaseRequest
return Roact.createFragment({
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptPurchaseRequested,
callback = onPurchaseRequest,
}),
RobloxPurchase = GetFFlagPromptRobloxPurchaseEnabled() and Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptRobloxPurchaseRequested,
callback = onRobloxPurchaseRequest,
}),
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptProductPurchaseRequested,
callback = onProductPurchaseRequest,
}),
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptGamePassPurchaseRequested,
callback = onPurchaseGamePassRequest,
}),
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.ServerPurchaseVerification,
callback = onServerPurchaseVerification,
}),
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptBundlePurchaseRequested,
callback = onBundlePurchaseRequest,
}),
Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptPremiumPurchaseRequested,
callback = onPremiumPurchaseRequest,
}),
SubscriptionsPurchase = GetFFlagDeveloperSubscriptionsEnabled() and Roact.createElement(ExternalEventConnection, {
event = MarketplaceService.PromptSubscriptionPurchaseRequested,
callback = onSubscriptionPurchaseRequest,
})
})
end
MarketplaceServiceEventConnector = connectToStore(nil,
function(dispatch)
local function onPurchaseRequest(player, assetId, equipIfPurchased, currencyType)
if player == Players.LocalPlayer then
dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased, false))
end
end
local function onRobloxPurchaseRequest(assetId, equipIfPurchased)
dispatch(initiatePurchase(assetId, Enum.InfoType.Asset, equipIfPurchased, true))
end
local function onProductPurchaseRequest(player, productId, equipIfPurchased, currencyType)
if player == Players.LocalPlayer then
dispatch(initiatePurchase(productId, Enum.InfoType.Product, equipIfPurchased))
end
end
local function onPurchaseGamePassRequest(player, gamePassId)
if player == Players.LocalPlayer then
dispatch(initiatePurchase(gamePassId, Enum.InfoType.GamePass, false))
end
end
-- Specific to purchasing dev products
local function onServerPurchaseVerification(serverResponseTable)
if not serverResponseTable then
dispatch(ErrorOccurred(PurchaseError.UnknownFailure))
else
local playerId = serverResponseTable["playerId"]
if playerId ~= nil then
playerId = tonumber(serverResponseTable["playerId"])
end
if playerId == Players.LocalPlayer.UserId then
dispatch(completePurchase())
end
end
end
local function onBundlePurchaseRequest(player, bundleId)
if player == Players.LocalPlayer then
dispatch(initiateBundlePurchase(bundleId))
end
end
local function onPremiumPurchaseRequest(player)
if player == Players.LocalPlayer then
dispatch(initiatePremiumPurchase())
end
end
local function onSubscriptionPurchaseRequest(player, subscriptionId)
if player == Players.LocalPlayer then
dispatch(initiateSubscriptionPurchase(subscriptionId))
end
end
return {
onPurchaseRequest = onPurchaseRequest,
onRobloxPurchaseRequest = onRobloxPurchaseRequest,
onProductPurchaseRequest = onProductPurchaseRequest,
onPurchaseGamePassRequest = onPurchaseGamePassRequest,
onServerPurchaseVerification = onServerPurchaseVerification,
onBundlePurchaseRequest = onBundlePurchaseRequest,
onPremiumPurchaseRequest = onPremiumPurchaseRequest,
onSubscriptionPurchaseRequest = onSubscriptionPurchaseRequest,
}
end)(MarketplaceServiceEventConnector)
return MarketplaceServiceEventConnector
@@ -0,0 +1,40 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local t = require(LuaPackages.t)
local LocalizationService = require(Root.Localization.LocalizationService)
local LocalizationContextConsumer = require(script.Parent.LocalizationContextConsumer)
local validateProps = t.strictInterface({
keys = t.table,
render = t.callback,
})
local validateItem = t.strictInterface({
key = t.string,
params = t.optional(t.table),
})
local function MultiTextLocalizer(props)
assert(validateProps(props))
for _, item in pairs(props.keys) do
assert(validateItem(item))
end
local render = props.render
return Roact.createElement(LocalizationContextConsumer, {
render = function(localizationContext)
local textMap = {}
for key, item in pairs(props.keys) do
textMap[key] = LocalizationService.getString(localizationContext, item.key, item.params)
end
return render(textMap)
end,
})
end
return MultiTextLocalizer
@@ -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 Root = script.Parent.Parent.Parent
local MarketplaceService = game:GetService("MarketplaceService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local ErrorOccurred = require(Root.Actions.ErrorOccurred)
local PurchaseError = require(Root.Enums.PurchaseError)
local retryAfterUpsell = require(Root.Thunks.retryAfterUpsell)
local connectToStore = require(Root.connectToStore)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
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,24 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LocalizationService = require(Root.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,41 @@
--[[
Connects to MarketplaceService's callback for completing a native purchase, so that we can
retry after an upsell purchase was processed
]]
local Root = script.Parent.Parent.Parent
local Players = game:GetService("Players")
local ABTestService = game:GetService("ABTestService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local connectToStore = require(Root.connectToStore)
local ExternalEventConnection = require(script.Parent.ExternalEventConnection)
local function PlayerConnector(props)
local playerConnects = props.playerConnects
return Roact.createElement(ExternalEventConnection, {
event = Players.PlayerAdded,
callback = playerConnects,
})
end
local function mapDispatchToProps(dispatch)
return {
playerConnects = function(player)
if player == Players.LocalPlayer then
ABTestService:InitializeForUserId(Players.LocalPlayer.UserId)
end
end,
}
end
PlayerConnector = connectToStore(
nil,
mapDispatchToProps
)(PlayerConnector)
return PlayerConnector
@@ -0,0 +1,25 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LocalizationService = require(Root.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,23 @@
--[[
Helper for supplying the current Roblox Locale to the Roact
tree via context using the LocalizationContextProvider
]]
local Root = script.Parent.Parent.Parent
local LocalizationService = game:GetService("LocalizationService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local getLocalizationContext = require(Root.Localization.getLocalizationContext)
local LocalizationContextProvider = require(script.Parent.LocalizationContextProvider)
local function provideRobloxLocale(renderFunc)
return Roact.createElement(LocalizationContextProvider, {
localizationContext = getLocalizationContext(LocalizationService.RobloxLocaleId),
render = renderFunc
})
end
return provideRobloxLocale
@@ -0,0 +1,18 @@
--[[
Helpful wrapper around LayoutValuesConsumer to make it a
little less verbose to use
]]
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local LayoutValuesConsumer = require(script.Parent.LayoutValuesConsumer)
local function withLayoutValues(renderFunc)
return Roact.createElement(LayoutValuesConsumer, {
render = renderFunc,
})
end
return withLayoutValues
@@ -0,0 +1,52 @@
local Root = script.Parent.Parent.Parent
local TextService = game:GetService("TextService")
local LuaPackages = Root.Parent
local UIBlox = require(LuaPackages.UIBlox)
local Roact = require(LuaPackages.Roact)
local t = require(LuaPackages.t)
local withStyle = UIBlox.Style.withStyle
local AutoSizedText = Roact.PureComponent:extend("AutoSizedText")
local validateProps = t.strictInterface({
text = t.string,
width = t.number,
layoutOrder = t.number,
})
function AutoSizedText:render()
assert(validateProps(self.props))
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
local fonts = stylePalette.Font
local text = self.props.text
local textSize = fonts.Body.RelativeSize * fonts.BaseSize
local font = fonts.Body.Font
local totalTextSize
if text ~= nil then
totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(self.props.width, 10000))
else
totalTextSize = Vector2.new(0, 0)
end
return Roact.createElement("TextLabel", {
Size = UDim2.new(1, 0, 0, totalTextSize.Y),
BackgroundTransparency = 1,
Text = text,
TextSize = textSize,
TextColor3 = theme.TextDefault.Color,
TextTransparency = theme.TextDefault.Transparency,
Font = font,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
LayoutOrder = self.props.layoutOrder,
})
end)
end
return AutoSizedText
@@ -0,0 +1,23 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.Test.UnitTestContainer)
local AutoSizedText = require(script.Parent.AutoSizedText)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(AutoSizedText, {
text = "Test",
width = 0,
layoutOrder = 0,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,73 @@
local Root = script.Parent.Parent.Parent
local TextService = game:GetService("TextService")
local LuaPackages = Root.Parent
local UIBlox = require(LuaPackages.UIBlox)
local Roact = require(LuaPackages.Roact)
local t = require(LuaPackages.t)
local Cryo = require(LuaPackages.Cryo)
local Images = UIBlox.App.ImageSet.Images
local withStyle = UIBlox.Style.withStyle
local IconSize = UIBlox.App.Constant.IconSize
local CHECK_ICON = "icons/status/success_small"
local TEXT_LEFT_PADDING = IconSize.Small + 16
local BulletPoint = Roact.PureComponent:extend("BulletPoint")
local validateProps = t.strictInterface({
text = t.string,
width = t.number,
layoutOrder = t.number,
})
function BulletPoint:render()
assert(validateProps(self.props))
return withStyle(function(stylePalette)
local theme = stylePalette.Theme
local fonts = stylePalette.Font
local text = self.props.text
local textSize = fonts.Body.RelativeSize * fonts.BaseSize
local font = fonts.Body.Font
local totalTextSize
if text ~= nil then
totalTextSize = TextService:GetTextSize(text, textSize, font,
Vector2.new(self.props.width - TEXT_LEFT_PADDING, 10000))
else
totalTextSize = Vector2.new(0, 0)
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, totalTextSize.Y),
BackgroundTransparency = 1,
LayoutOrder = self.props.layoutOrder,
} , {
Roact.createElement("ImageLabel", Cryo.Dictionary.join(Images[CHECK_ICON], {
Position = UDim2.new(0, 2, 0, 2),
Size = UDim2.new(0, IconSize.Small, 0, IconSize.Small),
ImageColor3 = theme.IconDefault.Color,
ImageTransparency = theme.IconDefault.Transparency,
BackgroundTransparency = 1,
})),
Roact.createElement("TextLabel", {
Position = UDim2.new(0, TEXT_LEFT_PADDING, 0, 0),
Size = UDim2.new(1, -TEXT_LEFT_PADDING, 0, totalTextSize.Y),
BackgroundTransparency = 1,
Text = text,
TextSize = textSize,
TextColor3 = theme.TextDefault.Color,
TextTransparency = theme.TextDefault.Transparency,
Font = font,
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
LayoutOrder = self.props.layoutOrder,
})
})
end)
end
return BulletPoint
@@ -0,0 +1,23 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.Test.UnitTestContainer)
local BulletPoint = require(script.Parent.BulletPoint)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(BulletPoint, {
text = "Test",
width = 0,
layoutOrder = 0,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,209 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Cryo = require(LuaPackages.Cryo)
local FitFrame = require(LuaPackages.FitFrame)
local FitFrameVertical = FitFrame.FitFrameVertical
local UIBlox = require(LuaPackages.UIBlox)
local PartialPageModal = UIBlox.App.Dialog.Modal.PartialPageModal
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local Images = UIBlox.App.ImageSet.Images
local PromptState = require(Root.Enums.PromptState)
local launchPremiumUpsell = require(Root.Thunks.launchPremiumUpsell)
local hideWindow = require(Root.Thunks.hideWindow)
local connectToStore = require(Root.connectToStore)
local MultiTextLocalizer = require(script.Parent.Parent.Connection.MultiTextLocalizer)
local AutoSizedText = require(script.Parent.AutoSizedText)
local BulletPoint = require(script.Parent.BulletPoint)
local PREMIUM_ICON = "icons/graphic/premium_large"
local PremiumModal = Roact.Component:extend(script.Name)
local PREMIUM_MODAL_LOC_KEY = "CoreScripts.PremiumModal.%s"
local CONTENT_PADDING = 24
local CONDENSED_CONTENT_PADDING = 12
local ICON_SIZE = 80
local CONDENSED_ICON_SIZE = 48
-- TODO(esauer):
-- Make the 120 calculation come directly from UIBlox Modal
-- self.updateContentSizes should automatically know how to make the calculation
function PremiumModal:init()
self.isCondensed = false
-- Hack :( The modal should tell you what the width is, this just prevents having to render
-- 540 (Max modal width) - 24 * 2 (Side paddings)
self.contentSize = Vector2.new(self.props.screenSize.X > 492 and 492 or self.props.screenSize.X, 0)
self.contentSizes, self.changeContentSizes = Roact.createBinding({
padding = UDim.new(0, CONTENT_PADDING),
iconSize = UDim2.new(1, 0, 0, ICON_SIZE)
})
self.purchasePremium = function()
self.props.purchasePremium()
end
self.updateContentSizes = function()
-- 120 is the height of the components of the modal not including the customized content
if self.isCondensed then
self.isCondensed = self.props.screenSize.Y < self.contentSize.Y + 120
+ ICON_SIZE - CONDENSED_ICON_SIZE
+ (CONTENT_PADDING - CONDENSED_CONTENT_PADDING) * 2
else
self.isCondensed = self.props.screenSize.Y <= self.contentSize.Y + 120
end
self.changeContentSizes({
padding = UDim.new(0, self.isCondensed and CONDENSED_CONTENT_PADDING or CONTENT_PADDING),
iconSize = UDim2.new(1, 0, 0, self.isCondensed and CONDENSED_ICON_SIZE or ICON_SIZE)
})
end
end
function PremiumModal:didUpdate(prevProps)
if self.props.screenSize ~= prevProps.screenSize then
self.updateContentSizes()
end
end
function PremiumModal:render()
local promptState = self.props.promptState
local premiumProductInfo = self.props.premiumProductInfo
local screenSize = self.props.screenSize
return Roact.createElement(MultiTextLocalizer, {
keys = {
titleLocalizedText = {
key = PREMIUM_MODAL_LOC_KEY:format("Title.PremiumRequired"),
},
monthlyLocalizedText = {
key = PREMIUM_MODAL_LOC_KEY:format("Action.PricePerMonth"),
params = {
price = premiumProductInfo.currencySymbol..tostring(premiumProductInfo.price)
},
},
descLocalizedText = {
key = PREMIUM_MODAL_LOC_KEY:format("Body.Description"),
},
bulletPoint1Text = {
key = PREMIUM_MODAL_LOC_KEY:format("Body.RobuxMonthlyV2"),
params = {
robux = premiumProductInfo.robuxAmount
},
},
bulletPoint2Text = {
key = PREMIUM_MODAL_LOC_KEY:format("Body.PremiumOnlyAreas"),
},
bulletPoint3Text = {
key = PREMIUM_MODAL_LOC_KEY:format("Body.RobuxDiscount"),
},
},
render = function(locTextMap)
return Roact.createElement(PartialPageModal, {
title = locTextMap.titleLocalizedText,
screenSize = screenSize,
buttonStackProps = {
buttons = {
{
buttonType = ButtonType.PrimarySystem,
props = {
isDisabled = promptState ~= PromptState.PremiumUpsell,
onActivated = self.purchasePremium,
text = locTextMap.monthlyLocalizedText,
},
},
},
buttonHeight = 48,
},
onCloseClicked = self.props.setHideWindow
}, {
Roact.createElement(FitFrameVertical, {
BackgroundTransparency = 1,
width = UDim.new(1, 0),
contentPadding = UDim.new(0, 24),
margin = {
top = 24,
bottom = 24,
},
[Roact.Change.AbsoluteSize] = function(rbx)
self.contentSize = rbx.AbsoluteSize
self.updateContentSizes()
end
}, {
Icon = Roact.createElement("ImageLabel", Cryo.Dictionary.join(Images[PREMIUM_ICON], {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = self.contentSizes:map(function(values)
return values.iconSize
end),
ScaleType = Enum.ScaleType.Fit,
BackgroundTransparency = 1,
LayoutOrder = 1,
})),
Roact.createElement(AutoSizedText, {
text = locTextMap.descLocalizedText,
width = self.contentSize.X,
layoutOrder = 2,
}),
Roact.createElement(FitFrameVertical, {
BackgroundTransparency = 1,
LayoutOrder = 3,
width = UDim.new(1, 0),
contentPadding = self.contentSizes:map(function(values)
return values.padding
end),
}, {
Bullet1 = Roact.createElement(BulletPoint, {
text = locTextMap.bulletPoint1Text,
width = self.contentSize.X,
layoutOrder = 1,
}),
Bullet2 = Roact.createElement(BulletPoint, {
text = locTextMap.bulletPoint2Text,
width = self.contentSize.X,
layoutOrder = 2,
}),
Bullet3 = Roact.createElement(BulletPoint, {
text = locTextMap.bulletPoint3Text,
width = self.contentSize.X,
layoutOrder = 3,
})
}),
})
})
end
})
end
local function mapStateToProps(state)
return {
premiumProductInfo = state.premiumProductInfo,
promptState = state.promptState,
requestType = state.promptRequest.requestType,
purchaseError = state.purchaseError,
windowState = state.windowState,
}
end
local function mapDispatchToProps(dispatch)
return {
purchasePremium = function()
dispatch(launchPremiumUpsell())
end,
setHideWindow = function()
dispatch(hideWindow())
end,
}
end
PremiumModal = connectToStore(
mapStateToProps,
mapDispatchToProps
)(PremiumModal)
return PremiumModal
@@ -0,0 +1,43 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Rodux = require(LuaPackages.Rodux)
local PromptState = require(Root.Enums.PromptState)
local RequestType = require(Root.Enums.RequestType)
local WindowState = require(Root.Enums.WindowState)
local Reducer = require(Root.Reducers.Reducer)
local UnitTestContainer = require(Root.Test.UnitTestContainer)
local PremiumModal = require(script.Parent.PremiumModal)
PremiumModal = PremiumModal.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, {
overrideStore = Rodux.Store.new(Reducer)
}, {
Roact.createElement(PremiumModal, {
premiumProductInfo = {
premiumFeatureTypeName = "Subscription",
mobileProductId = "com.roblox.robloxmobile.RobloxPremium450",
description = "Roblox Premium 450",
price = 4.99,
robuxAmount = 450,
isSubscriptionOnly = false,
currencySymbol = "$",
},
promptState = PromptState.PremiumUpsell,
promptRequest = {
requestType = RequestType.Premium
},
windowState = WindowState.Hidden,
screenSize = Vector2.new(100, 100)
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,206 @@
local Root = script.Parent.Parent.Parent
local GuiService = game:GetService("GuiService")
local ContextActionService = game:GetService("ContextActionService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Otter = require(LuaPackages.Otter)
local UIBlox = require(LuaPackages.UIBlox)
local InteractiveAlert = UIBlox.App.Dialog.Alert.InteractiveAlert
local ButtonType = UIBlox.App.Button.Enum.ButtonType
local PurchaseError = require(Root.Enums.PurchaseError)
local PromptState = require(Root.Enums.PromptState)
local RequestType = require(Root.Enums.RequestType)
local WindowState = require(Root.Enums.WindowState)
local completeRequest = require(Root.Thunks.completeRequest)
local launchPremiumUpsell = require(Root.Thunks.launchPremiumUpsell)
local hideWindow = require(Root.Thunks.hideWindow)
local connectToStore = require(Root.connectToStore)
local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection)
local MultiTextLocalizer = require(script.Parent.Parent.Connection.MultiTextLocalizer)
local PremiumModal = require(script.Parent.PremiumModal)
local PREMIUM_MODAL_LOC_KEY = "CoreScripts.PremiumModal.%s"
local PREMIUM_BUTTON_BIND = "PremiumBackButton"
local PremiumPrompt = Roact.Component:extend(script.Name)
local SPRING_CONFIG = {
dampingRatio = 1,
frequency = 1.6,
}
local function isRelevantRequestType(requestType)
return requestType == RequestType.Premium
end
function PremiumPrompt:init()
self.state = {
screenSize = Vector2.new(0, 0),
}
self.changeScreenSize = function(rbx)
if self.state.screenSize ~= rbx.AbsoluteSize then
self:setState({
screenSize = rbx.AbsoluteSize,
})
end
end
local animationProgress, setProgress = Roact.createBinding(0)
self.motor = Otter.createSingleMotor(0)
self.motor:start()
self.motor:onStep(setProgress)
self.animationProgress = animationProgress
self.motor:onComplete(function()
-- Do not complete the request if we are waiting for the browser or native purchase to complete
if self.props.windowState == WindowState.Hidden
and isRelevantRequestType(self.props.requestType)
and self.props.promptState ~= PromptState.UpsellInProgress then
self.props.setCompleteRequest()
end
end)
self.onClose = function()
self.props.hideWindow()
end
end
function PremiumPrompt:didUpdate(prevProps, prevState)
if prevProps.windowState ~= self.props.windowState then
local goal = (self.props.windowState == WindowState.Hidden or not isRelevantRequestType(self.props.requestType)) and 0 or 1
self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG))
if self.props.windowState == WindowState.Shown then
ContextActionService:BindCoreAction(
PREMIUM_BUTTON_BIND,
function(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin then
self.onClose()
end
end, false, Enum.KeyCode.ButtonB)
else
ContextActionService:UnbindCoreAction(PREMIUM_BUTTON_BIND)
end
end
end
function PremiumPrompt:RenderError()
local purchaseError = self.props.purchaseError
local errorKey
if purchaseError == PurchaseError.AlreadyPremium then
errorKey = "Error.AlreadyPremium"
elseif purchaseError == PurchaseError.PremiumUnavailablePlatform then
errorKey = "Error.PlatformUnavailable"
else
errorKey = "Error.Unavailable"
end
return Roact.createElement(MultiTextLocalizer, {
keys = {
titleLocalizedText = {
key = PREMIUM_MODAL_LOC_KEY:format("Title.Error"),
},
errorLocalizedText = {
key = PREMIUM_MODAL_LOC_KEY:format(errorKey),
},
okLocalizedText = {
key = "CoreScripts.PurchasePrompt.Button.OK",
},
},
render = function(textMap)
return Roact.createElement(InteractiveAlert, {
bodyText = textMap.errorLocalizedText,
buttonStackInfo = {
buttons = {
{
buttonType = ButtonType.PrimarySystem,
props = {
onActivated = self.onClose,
text = textMap.okLocalizedText,
},
},
},
},
screenSize = self.state.screenSize,
title = textMap.titleLocalizedText,
})
end
})
end
function PremiumPrompt:render()
local promptState = self.props.promptState
local requestType = self.props.requestType
local contents
if promptState == PromptState.None or not isRelevantRequestType(requestType) 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
else
if promptState == PromptState.Error then
contents = self:RenderError()
else
contents = Roact.createElement(PremiumModal,{
screenSize = self.state.screenSize,
})
end
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0.5, 0),
Position = self.animationProgress:map(function(value)
return UDim2.new(0.5, 0, 1 - value, 0)
end),
[Roact.Change.AbsoluteSize] = self.changeScreenSize,
BackgroundTransparency = 1,
}, {
PremiumPrompt = contents,
OnCoreGuiMenuOpened = Roact.createElement(ExternalEventConnection, {
event = GuiService.MenuOpened,
callback = self.onClose,
})
})
end
local function mapStateToProps(state)
return {
premiumProductInfo = state.premiumProductInfo,
promptState = state.promptState,
requestType = state.promptRequest.requestType,
purchaseError = state.purchaseError,
windowState = state.windowState,
}
end
local function mapDispatchToProps(dispatch)
return {
purchasePremium = function()
dispatch(launchPremiumUpsell())
end,
hideWindow = function()
dispatch(hideWindow())
end,
setCompleteRequest = function()
dispatch(completeRequest())
end
}
end
PremiumPrompt = connectToStore(
mapStateToProps,
mapDispatchToProps
)(PremiumPrompt)
return PremiumPrompt
@@ -0,0 +1,42 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Rodux = require(LuaPackages.Rodux)
local PromptState = require(Root.Enums.PromptState)
local RequestType = require(Root.Enums.RequestType)
local WindowState = require(Root.Enums.WindowState)
local Reducer = require(Root.Reducers.Reducer)
local UnitTestContainer = require(Root.Test.UnitTestContainer)
local PremiumPrompt = require(script.Parent.PremiumPrompt)
PremiumPrompt = PremiumPrompt.getUnconnected()
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, {
overrideStore = Rodux.Store.new(Reducer)
}, {
Roact.createElement(PremiumPrompt, {
premiumProductInfo = {
premiumFeatureTypeName = "Subscription",
mobileProductId = "com.roblox.robloxmobile.RobloxPremium450",
description = "Roblox Premium 450",
price = 4.99,
robuxAmount = 450,
isSubscriptionOnly = false,
currencySymbol = "$",
},
promptState = PromptState.PremiumUpsell,
promptRequest = {
requestType = RequestType.Premium
},
windowState = WindowState.Hidden,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,119 @@
local Root = script.Parent.Parent.Parent
local UserInputService = game:GetService("UserInputService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local PromptState = require(Root.Enums.PromptState)
local UpsellFlow = require(Root.Enums.UpsellFlow)
local LocalizationService = require(Root.Localization.LocalizationService)
local getUpsellFlow = require(Root.NativeUpsell.getUpsellFlow)
local isMockingPurchases = require(Root.Utils.isMockingPurchases)
local getPlayerPrice = require(Root.Utils.getPlayerPrice)
local connectToStore = require(Root.connectToStore)
local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
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.AdditionalDetailsLabel,
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.AdditionalDetailsLabel,
BackgroundTransparency = 1,
BorderSizePixel = 0,
TextColor3 = Color3.new(1, 1, 1),
Font = Enum.Font.SourceSans,
TextSize = values.TextSize.AdditionalDetails,
TextYAlignment = Enum.TextYAlignment.Top,
TextScaled = true,
TextWrapped = true,
}, {
TextSizeConstraint = Roact.createElement("UITextSizeConstraint", {
MaxTextSize = values.TextSize.AdditionalDetails,
})
})
end,
})
end)
end
local function mapStateToProps(state)
local promptState = state.promptState
local messageKey = nil
local messageParams = nil
local isPlayerPremium = state.accountInfo.membershipType == 4
local price = getPlayerPrice(state.productInfo, isPlayerPremium)
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("BalanceFutureV2")
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.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,35 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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,38 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,18 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,82 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Cryo = require(LuaPackages.Cryo)
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 = Cryo.Dictionary.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,17 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,38 @@
local Root = script.Parent.Parent.Parent
local TextService = game:GetService("TextService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Cryo = require(LuaPackages.Cryo)
local AutoSizedTextLabel = Roact.PureComponent:extend("AutoSizedTextLabel")
function AutoSizedTextLabel:render()
local text = self.props.Text
local textSize = self.props.TextSize
local font = self.props.Font
local width = self.props.width
local maxHeight = self.props.maxHeight
local totalTextSize
if text ~= nil then
totalTextSize = TextService:GetTextSize(text, textSize, font, Vector2.new(width, 10000))
else
totalTextSize = Vector2.new(0, 0)
end
local height = totalTextSize.Y
if maxHeight and height > maxHeight then
height = maxHeight
end
local textLabelProps = Cryo.Dictionary.join(self.props, {
width = Cryo.None,
maxHeight = Cryo.None,
Size = UDim2.new(0, width, 0, height),
})
return Roact.createElement("TextLabel", textLabelProps)
end
return AutoSizedTextLabel
@@ -0,0 +1,46 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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 not throw even if no text is provided", function()
local element = Roact.createElement(AutoSizedTextLabel, {
TextSize = 10,
Font = Enum.Font.SourceSans,
width = 100,
})
expect(Roact.mount(element)).to.be.ok()
end)
it("should clamp its height if maxHeight was provided", function()
local element = Roact.createElement(AutoSizedTextLabel, {
Text = "Really long text that should get to a height larger than 1 line!",
TextSize = 10,
Font = Enum.Font.SourceSans,
width = 100,
maxHeight = 2,
})
local folder = Instance.new("Frame")
Roact.mount(element, folder)
local textLabel = folder:FindFirstChildWhichIsA("GuiObject")
expect(textLabel.Size.Y.Offset).to.equal(2)
end)
end
@@ -0,0 +1,161 @@
local Root = script.Parent.Parent.Parent
local ContextActionService = game:GetService("ContextActionService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local connectToStore = require(Root.connectToStore)
local TextLocalizer = require(script.Parent.Parent.Connection.TextLocalizer)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
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,
function(actionName, inputState, inputObj)
--[[
CLILUACORE-521:
InputState MUST be 'Begin' in this case; otherwise, opening
the settings menu it will create new ContextActionService
bindings. When it does this, they trigger the 'Cancel' input
state, which invoke our binding (in order to tell it that
it's being canceled)
]]
if inputState == Enum.UserInputState.Begin then
self.props.onClick()
end
end,
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,50 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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 Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,22 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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 Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local ClickScamDetector = require(Root.Utils.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,23 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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,62 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,22 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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,94 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Cryo = require(LuaPackages.Cryo)
local Roact = require(LuaPackages.Roact)
local UIBlox = require(LuaPackages.UIBlox)
local UIBloxImages = UIBlox.App.ImageSet.Images
local PurchaseError = require(Root.Enums.PurchaseError)
local PromptState = require(Root.Enums.PromptState)
local connectToStore = require(Root.connectToStore)
local PREMIUM_ICON = UIBloxImages["icons/graphic/premium_large"]
local ADULT_ERROR_ICON = UIBloxImages["icons/status/error_large"]
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local function ItemPreviewImage(props)
return withLayoutValues(function(values)
local layoutOrder = props.layoutOrder
local promptState = props.promptState
local purchaseError = props.purchaseError
local productImageUrl = props.productImageUrl
local showPremiumIcon = false
local showAdultErrorIcon = false
local backgroundTransparency = 0
if promptState == PromptState.AdultConfirmation then
showAdultErrorIcon = true
backgroundTransparency = 1
elseif promptState == PromptState.Error then
if purchaseError == PurchaseError.PremiumOnly then
showPremiumIcon = true
else
productImageUrl = values.Image.ErrorIcon.Path
end
end
return Roact.createElement("Frame", {
Size = values.Size.ItemPreviewContainerFrame,
BackgroundTransparency = 1,
BorderSizePixel = 0,
LayoutOrder = layoutOrder,
}, {
PremiumIcon = showPremiumIcon and Roact.createElement("ImageLabel", Cryo.Dictionary.join(PREMIUM_ICON, {
Size = values.Size.ItemPreview,
BackgroundTransparency = 1,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
ImageTransparency = 0,
})) or nil,
ItemPreviewImageContainer = not showPremiumIcon and Roact.createElement("Frame", {
Size = values.Size.ItemPreviewWhiteFrame,
BackgroundTransparency = backgroundTransparency,
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 = showAdultErrorIcon and Roact.createElement("ImageLabel", Cryo.Dictionary.join(ADULT_ERROR_ICON, {
Size = values.Size.ItemPreview,
BackgroundTransparency = 1,
BorderSizePixel = 0,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
ImageTransparency = 0,
}))
or 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,
ImageTransparency = 0,
}),
}) or nil,
})
end)
end
local function mapStateToProps(state)
return {
promptState = state.promptState,
purchaseError = state.purchaseError,
productImageUrl = state.productInfo.imageUrl,
}
end
ItemPreviewImage = connectToStore(
mapStateToProps
)(ItemPreviewImage)
return ItemPreviewImage
@@ -0,0 +1,24 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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 Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,22 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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,60 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local NumberLocalizer = require(script.Parent.Parent.Connection.NumberLocalizer)
local AutoResizeList = require(script.Parent.AutoResizeList)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
return function(props)
return withLayoutValues(function(values)
local layoutOrder = props.layoutOrder
local price = props.price
return 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 = values.TextColor.PriceLabel,
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
@@ -0,0 +1,23 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.Test.UnitTestContainer)
local Price = require(script.Parent.Price)
it("should create and destroy without errors", function()
local element = Roact.createElement(UnitTestContainer, nil, {
Roact.createElement(Price, {
layoutOrder = 1,
imageUrl = "",
price = 50,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,148 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local PromptState = require(Root.Enums.PromptState)
local PurchaseError = require(Root.Enums.PurchaseError)
local LocalizationService = require(Root.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 getPlayerPrice = require(Root.Utils.getPlayerPrice)
local connectToStore = require(Root.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
local showPrice = props.showPrice
local price = props.price
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,
maxHeight = showPrice
and values.Size.ProductDescription.Y.Offset - values.Size.RobuxIconContainerFrame.Y.Offset
or values.Size.ProductDescription.Y.Offset,
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 = showPrice and Roact.createElement(Price, {
layoutOrder = 2,
price = price,
}) or nil,
})
end)
end
local function mapStateToProps(state)
local promptState = state.promptState
local isPlayerPremium = state.accountInfo.membershipType == 4
local price = getPlayerPrice(state.productInfo, isPlayerPremium)
local isFree = price == 0
local canPurchase = promptState ~= PromptState.Error
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(
price - state.accountInfo.balance
),
ASSET_TYPE = LocalizationService.nestedKeyParam(
LocalizationService.getKeyFromItemType(state.productInfo.itemType))
}
elseif promptState == PromptState.RobuxUpsell then
descriptionKey = PURCHASE_MESSAGE_KEY:format("NeedMoreRobux")
descriptionParams = {
ITEM_NAME = state.productInfo.name,
NEEDED_AMOUNT = LocalizationService.numberParam(
price - state.accountInfo.balance
),
ASSET_TYPE = LocalizationService.nestedKeyParam(
LocalizationService.getKeyFromItemType(state.productInfo.itemType))
}
elseif promptState == PromptState.AdultConfirmation then
descriptionKey = PURCHASE_MESSAGE_KEY:format("AdultConfirmation")
elseif 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.None 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.getKeyFromItemType(state.productInfo.itemType)),
ITEM_NAME = state.productInfo.name,
}
end
end
return {
descriptionKey = descriptionKey,
descriptionParams = descriptionParams,
showPrice = not isFree and canPurchase and promptState ~= PromptState.AdultConfirmation,
price = price,
}
end
ProductDescription = connectToStore(
mapStateToProps
)(ProductDescription)
return ProductDescription
@@ -0,0 +1,31 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Rodux = require(LuaPackages.Rodux)
local Reducer = require(Root.Reducers.Reducer)
local UnitTestContainer = require(Root.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,100 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local PromptState = require(Root.Enums.PromptState)
local purchaseItem = require(Root.Thunks.purchaseItem)
local launchRobuxUpsell = require(Root.Thunks.launchRobuxUpsell)
local getPlayerPrice = require(Root.Utils.getPlayerPrice)
local connectToStore = require(Root.connectToStore)
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 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 children
if promptState == PromptState.PurchaseComplete
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("BuyRobuxV2")
leftButtonCallback = onRobuxUpsell
elseif promptState == PromptState.AdultConfirmation then
confirmButtonStringKey = "CoreScripts.PurchasePrompt.Button.OK"
leftButtonCallback = onRobuxUpsell
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)
local isPlayerPremium = state.accountInfo.membershipType == 4
local price = getPlayerPrice(state.productInfo, isPlayerPremium)
return {
promptState = state.promptState,
price = price,
}
end
local function mapDispatchToProps(dispatch)
return {
onBuy = function()
dispatch(purchaseItem())
end,
onRobuxUpsell = function()
dispatch(launchRobuxUpsell())
end,
}
end
PromptButtons = connectToStore(
mapStateToProps,
mapDispatchToProps
)(PromptButtons)
return PromptButtons
@@ -0,0 +1,44 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local PromptState = require(Root.Enums.PromptState)
local UnitTestContainer = require(Root.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,56 @@
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,39 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Rodux = require(LuaPackages.Rodux)
local PromptState = require(Root.Enums.PromptState)
local Reducer = require(Root.Reducers.Reducer)
local UnitTestContainer = require(Root.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,
itemType = 2,
},
})
}, {
Roact.createElement(PromptContents, {
layoutOrder = 1,
onClose = function()
end,
})
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,126 @@
local Root = script.Parent.Parent.Parent
local GuiService = game:GetService("GuiService")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Otter = require(LuaPackages.Otter)
local RequestType = require(Root.Enums.RequestType)
local PromptState = require(Root.Enums.PromptState)
local WindowState = require(Root.Enums.WindowState)
local hideWindow = require(Root.Thunks.hideWindow)
local completeRequest = require(Root.Thunks.completeRequest)
local connectToStore = require(Root.connectToStore)
local ExternalEventConnection = require(script.Parent.Parent.Connection.ExternalEventConnection)
local PromptContents = require(script.Parent.PromptContents)
local InProgressContents = require(script.Parent.InProgressContents)
local withLayoutValues = require(script.Parent.Parent.Connection.withLayoutValues)
local PurchasePrompt = Roact.Component:extend(script.Name)
local SPRING_CONFIG = {
dampingRatio = 1,
frequency = 1.6,
}
local function isRelevantRequestType(requestType)
return requestType == RequestType.Asset
or requestType == RequestType.Bundle
or requestType == RequestType.GamePass
or requestType == RequestType.Product
or requestType == RequestType.Subscription
end
function PurchasePrompt:init()
local animationProgress, setProgress = Roact.createBinding(0)
self.motor = Otter.createSingleMotor(0)
self.motor:start()
self.motor:onStep(setProgress)
self.animationProgress = animationProgress
self.motor:onComplete(function()
if self.props.windowState == WindowState.Hidden and isRelevantRequestType(self.props.requestType) then
self.props.completeRequest()
end
end)
self.onClose = function()
self.props.hideWindow()
end
end
function PurchasePrompt:didUpdate(prevProps, prevState)
if prevProps.windowState ~= self.props.windowState then
local goal = (self.props.windowState == WindowState.Hidden or not isRelevantRequestType(self.props.requestType)) and 0 or 1
self.motor:setGoal(Otter.spring(goal, SPRING_CONFIG))
end
end
function PurchasePrompt:render()
return withLayoutValues(function(values)
local promptState = self.props.promptState
local requestType = self.props.requestType
local contents
if promptState == PromptState.None or not isRelevantRequestType(requestType) 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,
AnchorPoint = self.animationProgress:map(function(value)
return Vector2.new(0.5, 1 - 0.5 * value)
end),
Position = self.animationProgress:map(function(value)
return UDim2.new(0.5, 0, 0.5 * value, 0)
end),
}, {
PromptContents = contents,
OnCoreGuiMenuOpened = Roact.createElement(ExternalEventConnection, {
event = GuiService.MenuOpened,
callback = self.onClose,
})
})
end)
end
local function mapStateToProps(state)
return {
promptState = state.promptState,
requestType = state.promptRequest.requestType,
windowState = state.windowState,
}
end
local function mapDispatchToProps(dispatch)
return {
hideWindow = function()
dispatch(hideWindow())
end,
completeRequest = function()
dispatch(completeRequest())
end
}
end
PurchasePrompt = connectToStore(
mapStateToProps,
mapDispatchToProps
)(PurchasePrompt)
return PurchasePrompt
@@ -0,0 +1,35 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Rodux = require(LuaPackages.Rodux)
local PromptState = require(Root.Enums.PromptState)
local Reducer = require(Root.Reducers.Reducer)
local UnitTestContainer = require(Root.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,
itemType = 2,
},
})
}, {
Roact.createElement(PurchasePrompt)
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,68 @@
local Root = script.Parent.Parent.Parent
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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,21 @@
return function()
local Root = script.Parent.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local UnitTestContainer = require(Root.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,79 @@
local Root = script.Parent.Parent
local CorePackages = game:GetService("CorePackages")
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.Roact)
local Rodux = require(LuaPackages.Rodux)
local RoactRodux = require(LuaPackages.RoactRodux)
local UIBlox = require(LuaPackages.UIBlox)
local StyleProvider = UIBlox.Style.Provider
local Reducer = require(Root.Reducers.Reducer)
local Network = require(Root.Services.Network)
local Analytics = require(Root.Services.Analytics)
local PlatformInterface = require(Root.Services.PlatformInterface)
local ExternalSettings = require(Root.Services.ExternalSettings)
local Thunk = require(Root.Thunk)
local PremiumPrompt = require(script.Parent.PremiumPrompt.PremiumPrompt)
local PurchasePrompt = require(script.Parent.PurchasePrompt.PurchasePrompt)
local EventConnections = require(script.Parent.Connection.EventConnections)
local LayoutValuesProvider = require(script.Parent.Connection.LayoutValuesProvider)
local provideRobloxLocale = require(script.Parent.Connection.provideRobloxLocale)
local DarkTheme = require(CorePackages.AppTempCommon.LuaApp.Style.Themes.DarkTheme)
local Gotham = require(CorePackages.AppTempCommon.LuaApp.Style.Fonts.Gotham)
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,
}),
}),
isTenFootInterface = externalSettings.isTenFootInterface(),
}
end
function PurchasePromptApp:render()
return provideRobloxLocale(function()
return Roact.createElement(RoactRodux.StoreProvider, {
store = self.state.store,
}, {
StyleProvider = Roact.createElement(StyleProvider, {
style = {
Theme = DarkTheme,
Font = Gotham,
},
}, {
PurchasePrompt = Roact.createElement(LayoutValuesProvider, {
isTenFootInterface = self.state.isTenFootInterface,
render = function()
return Roact.createElement("ScreenGui", {
AutoLocalize = false,
IgnoreGuiInset = true,
}, {
PremiumPromptUI = Roact.createElement(PremiumPrompt),
PurchasePromptUI = Roact.createElement(PurchasePrompt),
EventConnections = Roact.createElement(EventConnections),
})
end
})
})
})
end)
end
return PurchasePromptApp
@@ -0,0 +1,15 @@
return function()
local Root = script.Parent.Parent
local LuaPackages = Root.Parent
local Roact = require(LuaPackages.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
@@ -0,0 +1,13 @@
--[[
Enumeration of types of items that can be purchased.
]]
local createEnum = require(script.Parent.createEnum)
local ItemType = createEnum("ItemType", {
"Asset",
"GamePass",
"Product",
"Bundle",
})
return ItemType
@@ -0,0 +1,18 @@
--[[
Enumerated state of the purchase prompt
]]
local createEnum = require(script.Parent.createEnum)
local PromptState = createEnum("PromptState", {
"None",
"PremiumUpsell",
"RobuxUpsell",
"PromptPurchase",
"PurchaseInProgress",
"UpsellInProgress",
"AdultConfirmation",
"PurchaseComplete",
"Error",
})
return PromptState
@@ -0,0 +1,35 @@
--[[
Enumeration of all possible error states
]]
local createEnum = require(script.Parent.createEnum)
local PurchaseError = createEnum("PurchaseError", {
-- Pre-purchase network failures
"CannotGetBalance",
"CannotGetItemPrice",
-- Premium
"AlreadyPremium",
"PremiumUnavailable",
"PremiumUnavailablePlatform",
-- Item unvailable
"NotForSale",
"AlreadyOwn",
"PremiumOnly",
"Under13",
"Limited",
"Guest",
"ThirdPartyDisabled",
"NotEnoughRobux",
"NotEnoughRobuxXbox",
"NotEnoughRobuxNoUpsell",
-- Network-reported failures
"UnknownFailure",
"UnknownFailureNoItemName",
"PurchaseDisabled",
"InvalidFunds",
})
return PurchaseError
@@ -0,0 +1,13 @@
local createEnum = require(script.Parent.createEnum)
local RequestType = createEnum("RequestType", {
"None",
"Asset",
"Bundle",
"GamePass",
"Product",
"Premium",
"Subscription",
})
return RequestType
@@ -0,0 +1,11 @@
local createEnum = require(script.Parent.createEnum)
local UpsellFlow = createEnum("UpsellFlow", {
"Web",
"Mobile",
"Xbox",
"Unavailable",
"None",
})
return UpsellFlow
@@ -0,0 +1,8 @@
local createEnum = require(script.Parent.createEnum)
local WindowState = createEnum("WindowState", {
"Hidden",
"Shown",
})
return WindowState
@@ -0,0 +1,58 @@
--[[
An implementation of an enumerated type in Lua. Creates enumerated
types with uniquely identifiable values (using symbol)
Note that resulting enum object does not associate ordinals with its
values, and cannot be iterated through. It can, however, test if a provided
value is a member of its set of values with the `isMember` function
This is valuable for the purchase prompt because it relies heavily on
enumerated values to determine things like state and which errors occurred.
]]
local Root = script.Parent.Parent
local Symbol = require(Root.Symbols.Symbol)
local strict = require(Root.strict)
--[[
Returns a new enum type with the given name.
]]
local function createEnum(enumName, values)
assert(typeof(enumName) == "string", "Bad argument #1, expected string")
assert(typeof(values) == "table", "Bad argument #2, expected list of values")
local enumInternal = {}
for _, valueName in ipairs(values) do
assert(valueName ~= "isMember", "Shadowing 'isMember' function is not allowed")
assert(typeof(valueName) == "string", "Only string names are supported for enum types")
local enumValue = Symbol.named(valueName)
local asString = ("%s.%s"):format(enumName, valueName)
getmetatable(enumValue).__tostring = function()
return asString
end
enumInternal[valueName] = enumValue
end
function enumInternal.isMember(value)
if typeof(value) ~= "userdata" then
return false
end
for _, enumeratedValue in pairs(enumInternal) do
if value == enumeratedValue then
return true
end
end
return false
end
local enum = newproxy(true)
getmetatable(enum).__index = enumInternal
return strict(enumInternal, enumName)
end
return createEnum
@@ -0,0 +1,66 @@
return function()
local createEnum = require(script.Parent.createEnum)
describe("validation rules", function()
it("should throw errors if given invalid values", function()
expect(function()
createEnum(1, {})
end).to.throw()
expect(function()
createEnum("MyEnum", "not a table")
end).to.throw()
end)
it("should throw errors if provided table contains invalid values", function()
expect(function()
createEnum("IllegalShadowing", {
"isMember",
})
end).to.throw()
expect(function()
createEnum("MyEnum", {
"Test",
12,
})
end)
end)
end)
describe("enum properties", function()
it("should have a reasonable string format for debugging", function()
local MyEnum = createEnum("MyEnum", {
"Value",
})
expect(tostring(MyEnum.Value)).to.equal("MyEnum.Value")
end)
it("should provide an isMember function to check membership", function()
local MyEnum = createEnum("MyEnum", {
"Value",
})
expect(MyEnum.isMember(MyEnum.Value)).to.equal(true)
expect(MyEnum.isMember(newproxy(true))).to.equal(false)
expect(MyEnum.isMember("Value")).to.equal(false)
end)
it("should generate objects that are unique even when named the same", function()
local Enum1 = createEnum("MyEnum", {
"Value",
})
local Enum2 = createEnum("MyEnum", {
"Value",
})
expect(Enum1.isMember(Enum1.Value)).to.equal(true)
expect(Enum2.isMember(Enum2.Value)).to.equal(true)
expect(Enum1.isMember(Enum2.Value)).to.equal(false)
expect(Enum2.isMember(Enum1.Value)).to.equal(false)
end)
end)
end
@@ -0,0 +1,5 @@
game:DefineFastFlag("AdultConfirmationEnabledV4", false)
return function()
return game:GetFastFlag("AdultConfirmationEnabledV4")
end
@@ -0,0 +1,5 @@
game:DefineFastFlag("AdultConfirmationEnabledNew", false)
return function()
return game:GetFastFlag("AdultConfirmationEnabledNew")
end
@@ -0,0 +1,3 @@
return function()
return settings():GetFFlag("DeveloperSubscriptionsEnabled")
end

Some files were not shown because too many files have changed in this diff Show More