Clients/Client2018/content/internal/AppShell/Modules/Shell/StorePane.lua

585 lines
18 KiB
Lua

--[[
// StorePane.lua by Kip Turner
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local UserDataModule = require(ShellModules:FindFirstChild('UserData'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local CreateConfirmPrompt = require(ShellModules:FindFirstChild('ConfirmPrompt'))
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local PlatformCatalogData = require(ShellModules:FindFirstChild('PlatformCatalogData'))
local CurrencyWidgetModule = require(ShellModules:FindFirstChild('CurrencyWidget'))
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local RobuxBalanceOverlay = require(ShellModules:FindFirstChild('RobuxBalanceOverlay'))
local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay'))
local Errors = require(ShellModules:FindFirstChild('Errors'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local TextService = game:GetService('TextService')
local GuiService = game:GetService('GuiService')
local PlatformService;
pcall(function() PlatformService = game:GetService('PlatformService') end)
--[[ Constants ]]--
local GRID_COLUMNS = 3
local GRID_SIZE = UDim2.new(0, 1620, 0, 610)
local GRID_POSITION = UDim2.new(0, 40, 0, 65)
local GRID_PADDING = UDim2.new(0, 20, 0, 20)
local GRID_CELL_SIZE = UDim2.new(0, 520, 0, 285)
local DESCRIPTION_SIZE = UDim2.new(1, 0, 0, 50)
local PRICE_CORNER_OFFSET = Vector2.new(-15, -12)
local NO_ITEMS_MSG_POSITION = UDim2.new(0.1, 0, 0, 275)
local NO_ITEMS_MSG_SIZE = UDim2.new(0.8, 0, 0, 150)
local ROBUX_ASSETS =
{
{
Wide = 'Robux01.png';
Square = 'RobuxSquare01.png';
};
{
Wide = 'Robux02.png';
Square = 'RobuxSquare02.png';
};
{
Wide = 'Robux03.png';
Square = 'RobuxSquare03.png';
};
{
Wide = 'Robux04.png';
Square = 'RobuxSquare04.png';
};
{
Wide = 'Robux05.png';
Square = 'RobuxSquare05.png';
};
{
Wide = 'Robux06.png';
Square = 'RobuxSquare06.png';
};
}
local function createGridItem(productInfo)
local this = {}
local container = Utility.Create'ImageButton'
{
Name = "StoreItemContainer",
BackgroundTransparency = 1,
BackgroundColor3 = Color3.new(220/255, 220/255, 220/255),
-- Image = '',
AutoButtonColor = false,
BorderSizePixel = 0,
ZIndex = 2;
AssetManager.CreateShadow(1);
SoundManager:CreateSound('MoveSelection');
}
local priceText = Utility.Create'TextLabel'
{
Name = "PriceText",
Size = UDim2.new(0, 0, 0, 0),
Position = UDim2.new(1, -15, 1, -12),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Right,
TextYAlignment = 'Bottom';
TextColor3 = GlobalSettings.WhiteTextColor,
Font = GlobalSettings.HeadingFont,
FontSize = GlobalSettings.LargeFontSize,
Text = '',
ZIndex = 2;
Parent = container,
}
local robuxIcon = Utility.Create'ImageLabel'
{
Name = 'RobuxIcon';
Position = UDim2.new(0,5,0,5);
Size = UDim2.new(0,80,0,80);
Image = 'rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png';
BackgroundTransparency = 1;
ZIndex = 2;
Parent = container;
};
local robuxAmount = Utility.Create'TextLabel'
{
Name = 'RobuxAmount';
Text = '';
Size = UDim2.new(0,0,1,0);
Position = UDim2.new(1,10,0,0);
TextXAlignment = 'Left';
TextColor3 = GlobalSettings.WhiteTextColor;
Font = GlobalSettings.BoldFont;
FontSize = GlobalSettings.LargeFontSize;
BackgroundTransparency = 1;
ZIndex = 2;
Parent = robuxIcon;
};
local percentMoreText = Utility.Create'TextLabel'
{
Name = "PercentMoreText",
Size = UDim2.new(0, 0, 0, 0),
Position = UDim2.new(0, 5, 1, 10),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextColor3 = GlobalSettings.GreenTextColor,
Font = GlobalSettings.BoldFont,
FontSize = GlobalSettings.ButtonSize,
-- Visible = false,
Text = '',
ZIndex = 2;
Parent = robuxIcon,
}
local function UpdateInfo()
local priceTextSize = TextService:GetTextSize(priceText.Text, Utility.ConvertFontSizeEnumToInt(priceText.FontSize), priceText.Font, Vector2.new())
priceText.Size = UDim2.new(0, priceTextSize.x , 0, priceTextSize.y)
priceText.Position = UDim2.new(1, PRICE_CORNER_OFFSET.x - priceTextSize.x, 1, PRICE_CORNER_OFFSET.y - priceTextSize.y)
end
UpdateInfo()
function this:GetContainer()
return container
end
function this:SetPrice(value)
priceText.Text = value
UpdateInfo()
end
function this:SetRobuxValue(value)
robuxAmount.Text = Utility.FormatNumberString(tostring(value))
end
function this:SetPercentMore(value)
percentMoreText.Visible = value > 0
percentMoreText.Text = string.format(Strings:LocalizedString('PercentMoreRobuxPhrase'), tostring(value))
end
function this:SetImage(image)
container.Image = image
end
return this
end
local function CreateStorePane(parent)
local this = {}
local gridItems = {}
local itemSet = {}
local storeItemClickConns = {}
local cachedBalance = nil
local cachedTotalBalance = nil
local inFocus = false
local currencyWidget = nil
local StorePaneContainer = Utility.Create'Frame'
{
Name = 'StorePane',
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Visible = false,
Parent = parent,
}
local StoreDescriptionText = Utility.Create'TextLabel'
{
Name = "StoreDescriptionText",
Size = DESCRIPTION_SIZE,
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = GlobalSettings.WhiteTextColor,
Font = GlobalSettings.LightFont,
FontSize = GlobalSettings.TitleSize,
TextWrapped = true,
Visible = false;
Text = Strings:LocalizedString('RobuxStoreDescription'),
Parent = StorePaneContainer,
}
local StoreNoItemsText = Utility.Create'TextLabel'
{
Name = "StoreNoItemsText",
Size = NO_ITEMS_MSG_SIZE,
Position = NO_ITEMS_MSG_POSITION,
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Center,
FontSize = GlobalSettings.TitleSize,
TextWrapped = true,
TextColor3 = GlobalSettings.GreyTextColor;
TextTransparency = GlobalSettings.FriendStatusTextTransparency;
Font = GlobalSettings.BoldFont;
Text = Strings:LocalizedString('RobuxStoreNoItemsPhrase'),
Visible = false;
Parent = StorePaneContainer,
}
local RobuxBalanceButton = Utility.Create'ImageButton'
{
Name = "RobuxBalanceButton";
-- size will change based on text bounds of balance
Size = UDim2.new(0, 436, 0, 75);
Position = UDim2.new(0, 0, 1, -100);
BackgroundTransparency = 1;
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.GreySelectionColor;
Selectable = false;
Parent = StorePaneContainer;
SoundManager:CreateSound('MoveSelection')
};
RobuxBalanceButton.SelectionGained:connect(function()
RobuxBalanceButton.BackgroundTransparency = 0;
end)
RobuxBalanceButton.SelectionLost:connect(function()
RobuxBalanceButton.BackgroundTransparency = 1;
end)
local RobuxHelpIcon = Utility.Create'ImageLabel'
{
Name = "RobuxHelpIcon";
Size = UDim2.new(0, 42, 0, 42);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/HelpIconSmall.png';
Visible = false;
Parent = RobuxBalanceButton;
AnchorPoint = Vector2.new(0, 0.5);
Position = UDim2.new(0, 10, 0.5, 4);
};
local function setBalanceButtonSize(balanceObjectSize)
local sizeX = 56 + RobuxHelpIcon.Size.X.Offset + (balanceObjectSize and balanceObjectSize.X or 0) + 10
RobuxBalanceButton.Size = UDim2.new(0, sizeX, 0, 75)
end
local showBalanceHelp = true
local showBalanceOverlayDebounce = false
RobuxBalanceButton.MouseButton1Click:connect(function()
if showBalanceOverlayDebounce then return end
--
showBalanceOverlayDebounce = true
if showBalanceHelp then
local robuxBalanceOverlay = RobuxBalanceOverlay(cachedBalance, cachedTotalBalance)
ScreenManager:OpenScreen(robuxBalanceOverlay, false)
end
showBalanceOverlayDebounce = false
end)
local function setBalanceHelpOption(platformBalance)
local totalBalance = UserDataModule.GetTotalUserBalanceAsync()
if totalBalance then
cachedTotalBalance = totalBalance
showBalanceHelp = platformBalance ~= totalBalance
RobuxHelpIcon.Visible = showBalanceHelp
RobuxBalanceButton.Selectable = showBalanceHelp
end
end
local function PopulateBalance()
spawn(function()
local platformBalance = currencyWidget and currencyWidget:GetRobuxAmountAsync() or UserDataModule.GetPlatformUserBalanceAsync()
if platformBalance then
cachedBalance = platformBalance
setBalanceHelpOption(platformBalance)
if currencyWidget then
setBalanceButtonSize(currencyWidget:GetAbsoluteSize())
end
else
Utility.DebugLog("Unable to update user's balance because web call failed.")
end
end)
end
local StoreContainer = Utility.Create'Frame'
{
Size = GRID_SIZE;
Name = "StoreContainer";
BackgroundTransparency = 1;
ClipsDescendants = true;
Position = GRID_POSITION;
Parent = StorePaneContainer;
}
local StoreUIGridLayout = Utility.Create'UIGridLayout'
{
Name = "StoreUIGridLayout";
CellPadding = GRID_PADDING;
CellSize = GRID_CELL_SIZE;
FillDirectionMaxCells = GRID_COLUMNS;
Parent = StoreContainer;
};
local function ContainsItem(gridItem)
return itemSet[gridItem] ~= nil
end
local function AddItem(gridItem)
if not ContainsItem(gridItem) then
table.insert(gridItems, gridItem)
itemSet[gridItem] = true
gridItem.Parent = StoreContainer
if GuiService.SelectedCoreObject == StoreContainer then
Utility.SetSelectedCoreObject(gridItem)
end
end
end
local SuccessfullyLoadedCatalog = false
local catalogLoading = false
local function OnLoad()
if catalogLoading or SuccessfullyLoadedCatalog then return end
catalogLoading = true
if PlatformService then
local catalogInfo, success, errormsg;
-- Hide these text labels while we are loading
StoreDescriptionText.Visible = false
StoreNoItemsText.Visible = false
local loader = LoadingWidget({Parent = StorePaneContainer}, {function() catalogInfo, success, errormsg = PlatformCatalogData:GetCatalogInfoAsync() end})
loader:AwaitFinished()
loader:Cleanup()
loader = nil
ScreenManager:DefaultFadeIn(StoreContainer)
if inFocus and GuiService.SelectedCoreObject == nil then
if gridItems[1] then
Utility.SetSelectedCoreObject(gridItems[1])
end
end
if success and catalogInfo then
while #storeItemClickConns > 0 do
Utility.DisconnectEvent(table.remove(storeItemClickConns, 1))
end
table.sort(catalogInfo, function(a, b)
local aPrice = PlatformCatalogData:ParseRobuxValue(a)
local bPrice = PlatformCatalogData:ParseRobuxValue(b)
if aPrice and bPrice then
return aPrice < bPrice
end
return a < b
end)
local worstRatio = nil
for _, productInfo in pairs(catalogInfo) do
local ratio = PlatformCatalogData:CalculateRobuxRatio(productInfo)
if Utility.IsFinite(ratio) and ratio ~= 0 then
if worstRatio == nil or ratio < worstRatio then
worstRatio = ratio
end
end
end
if #catalogInfo == 0 then
StoreDescriptionText.Visible = false
StoreNoItemsText.Visible = true
elseif not SuccessfullyLoadedCatalog then
SuccessfullyLoadedCatalog = true
StoreDescriptionText.Visible = true
StoreNoItemsText.Visible = false
local i = 1
for _, productInfo in pairs(catalogInfo) do
local productImageData = ROBUX_ASSETS[math.min(i, #ROBUX_ASSETS)]
local catalogItemImage = 'rbxasset://textures/ui/Shell/Images/Robux/' .. productImageData['Wide']
local confirmItemImage = 'rbxasset://textures/ui/Shell/Images/Robux/' .. productImageData['Square']
local debounce = false
local function onClick()
if debounce then return end
debounce = true
local confirmPrompt = CreateConfirmPrompt({ProductId = productInfo and productInfo.ProductId or "Unknown"; ProductName = productInfo and productInfo.Name or 'Unknown'; Balance = cachedBalance; Cost = productInfo and productInfo.DisplayPrice or "Unknown"; ProductImage = confirmItemImage; ProductImageSize = Vector2.new(484, 540); CurrencySymbol = '';},
{ShowRemainingBalance = false; ShowRobuxIcon = false; ConfirmWithPrice = true;})
do
local selectedObject = GuiService.SelectedCoreObject
if selectedObject and selectedObject:IsDescendantOf(StorePaneContainer) then
this.SavedSelection = selectedObject
end
end
ScreenManager:OpenScreen(confirmPrompt)
confirmPrompt:FadeInBackground()
local function onConfirmFinished(result)
if result == true then
Utility.DebugLog("Do buy")
local purchaseResult = nil
if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then
local purchaseCallSuccess, purchaseErrorMsg = pcall(function()
purchaseResult = PlatformService:BeginPlatformStorePurchase(productInfo.ProductId)
end)
if purchaseCallSuccess then
-- 0 means we bought it
if purchaseResult == 0 then
EventHub:dispatchEvent(EventHub.Notifications["RobuxCatalogPurchaseInitiated"], purchaseResult);
end
else
Utility.DebugLog("Purchase Robux failed with pcall status:" , purchaseCallSuccess , "and purchaseResult:" , purchaseResult , "because of:" , purchaseErrorMsg)
end
else
spawn(function()
ScreenManager:OpenScreen(ErrorOverlayModule(Errors.RobuxPurchase[1]), false)
end)
end
PopulateBalance()
Utility.DebugLog("Done with purchase; result was:" , purchaseResult)
else
Utility.DebugLog("Declined to buy")
end
end
-- confirmPrompt:AddResultCallback(onConfirmFinished)
local result = confirmPrompt:ResultAsync()
onConfirmFinished(result)
debounce = false
end
local extractedPrice = productInfo and productInfo.DisplayPrice or ""
local thisRatio = PlatformCatalogData:CalculateRobuxRatio(productInfo)
local item = createGridItem()
item:SetPrice(extractedPrice)
item:SetRobuxValue(PlatformCatalogData:ParseRobuxValue(productInfo))
if thisRatio and worstRatio then
item:SetPercentMore(math.floor(((thisRatio / worstRatio) - 1) * 100))
else
item:SetPercentMore(0)
end
item:SetImage(catalogItemImage)
AddItem(item:GetContainer())
table.insert(storeItemClickConns, item:GetContainer().MouseButton1Click:connect(onClick))
i = math.min(#ROBUX_ASSETS, i + 1)
end
end
else
StoreNoItemsText.Visible = true
Utility.DebugLog("StorePane - BeginGetCatalogInfo failed because:" , errormsg)
end
end
catalogLoading = false
end
if not SuccessfullyLoadedCatalog then
spawn(OnLoad)
end
--[[ Public API ]]--
function this:GetName()
return Strings:LocalizedString('CatalogWord')
end
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('StorePaneId')}
end
function this:IsFocused()
return inFocus
end
local RobuxChangedConn = nil
local robuxChangedEventCount = 0
local robuxAmountChangedLoader = nil
function this:Show()
StorePaneContainer.Visible = true
if not currencyWidget then
currencyWidget = CurrencyWidgetModule({Parent = RobuxBalanceButton; Position = UDim2.new(0, RobuxHelpIcon.Position.X.Offset + RobuxHelpIcon.Size.X.Offset + 10, 0.5, -30);})
else
spawn(function()
currencyWidget:RefreshRobuxAmountAsync()
end)
end
setBalanceButtonSize(currencyWidget:GetAbsoluteSize())
Utility.DisconnectEvent(RobuxChangedConn)
RobuxChangedConn = currencyWidget.RobuxChanged:connect(function()
PopulateBalance()
currencyWidget:GetRobuxAmountAsync()
setBalanceButtonSize(currencyWidget:GetAbsoluteSize())
SoundManager:Play('PurchaseSuccess')
end)
PopulateBalance()
self.TransitionTweens = ScreenManager:DefaultFadeIn(StorePaneContainer)
ScreenManager:PlayDefaultOpenSound()
if not SuccessfullyLoadedCatalog then
spawn(OnLoad)
end
end
function this:Hide()
StorePaneContainer.Visible = false
RobuxChangedConn = Utility.DisconnectEvent(RobuxChangedConn)
ScreenManager:DefaultCancelFade(self.TransitionTweens)
self.TransitionTweens = nil
-- Let's not do this it creates weird race conditions
-- if currencyWidget then
-- currencyWidget:Destroy()
-- currencyWidget = nil
-- end
end
function this:Focus()
-- TODO: What is the selection if the packages fail to load?
inFocus = true
if self.SavedSelection and self.SavedSelection:IsDescendantOf(StorePaneContainer) then
Utility.SetSelectedCoreObject(self.SavedSelection)
elseif gridItems[1] then
Utility.SetSelectedCoreObject(gridItems[1])
end
self.SavedSelection = nil
end
function this:RemoveFocus()
inFocus = false
local selectedObject = GuiService.SelectedCoreObject
if selectedObject and selectedObject:IsDescendantOf(StorePaneContainer) then
Utility.SetSelectedCoreObject(nil)
end
end
function this:SetPosition(newPosition)
StorePaneContainer.Position = newPosition
end
function this:SetParent(newParent)
StorePaneContainer.Parent = newParent
end
function this:IsAncestorOf(object)
return StorePaneContainer and StorePaneContainer:IsAncestorOf(object)
end
return this
end
return CreateStorePane