mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
1507 lines
51 KiB
Lua
1507 lines
51 KiB
Lua
--[[
|
|
// Filename: PurchasePromptScript2.lua
|
|
// Version 1.0
|
|
// Release 186
|
|
// Written by: jeditkacheff/jmargh
|
|
// Description: Handles in game purchases
|
|
]]--
|
|
--[[ Services ]]--
|
|
local GuiService = game:GetService('GuiService')
|
|
local HttpService = game:GetService('HttpService')
|
|
local HttpRbxApiService = game:GetService('HttpRbxApiService')
|
|
local InsertService = game:GetService('InsertService')
|
|
local MarketplaceService = game:GetService('MarketplaceService')
|
|
local Players = game:GetService('Players')
|
|
local UserInputService = game:GetService('UserInputService')
|
|
local RunService = game:GetService("RunService")
|
|
|
|
--[[ Script Variables ]]--
|
|
local RobloxGui = script.Parent
|
|
local ThirdPartyProductName = nil
|
|
|
|
--[[ Flags ]]--
|
|
local platform = UserInputService:GetPlatform()
|
|
local IsNativePurchasing = platform == Enum.Platform.XBoxOne or
|
|
platform == Enum.Platform.IOS or
|
|
platform == Enum.Platform.Android or
|
|
platform == Enum.Platform.UWP
|
|
|
|
local IsCurrentlyPrompting = false
|
|
local IsCurrentlyPurchasing = false
|
|
local IsPurchasingConsumable = false
|
|
local IsCheckingPlayerFunds = false
|
|
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
|
|
local TenFootInterface = require(RobloxGui.Modules.TenFootInterface)
|
|
local isTenFootInterface = TenFootInterface:IsEnabled()
|
|
local freezeControllerActionName = "doNothingActionPrompt"
|
|
local freezeThumbstick1Name = "doNothingThumbstickPrompt"
|
|
local freezeThumbstick2Name = "doNothingThumbstickPrompt"
|
|
local _,largeFont = pcall(function() return Enum.FontSize.Size42 end)
|
|
largeFont = largeFont or Enum.FontSize.Size36
|
|
local scaleFactor = 3
|
|
local purchaseState = nil
|
|
|
|
--[[ Purchase Data ]]--
|
|
local PurchaseData = {
|
|
AssetId = nil,
|
|
ProductId = nil,
|
|
CurrencyType = nil,
|
|
EquipOnPurchase = nil,
|
|
ProductInfo = nil,
|
|
ItemDescription = nil,
|
|
}
|
|
|
|
--[[ Constants ]]--
|
|
local BASE_URL = game:GetService('ContentProvider').BaseUrl:lower()
|
|
BASE_URL = string.gsub(BASE_URL, "/m.", "/www.")
|
|
local THUMBNAIL_URL = BASE_URL.."thumbs/asset.ashx?assetid="
|
|
-- Images
|
|
local BG_IMAGE = 'rbxasset://textures/ui/Modal.png'
|
|
local PURCHASE_BG = 'rbxasset://textures/ui/LoadingBKG.png'
|
|
local BUTTON_LEFT = 'rbxasset://textures/ui/ButtonLeft.png'
|
|
local BUTTON_LEFT_DOWN = 'rbxasset://textures/ui/ButtonLeftDown.png'
|
|
local BUTTON_RIGHT = 'rbxasset://textures/ui/ButtonRight.png'
|
|
local BUTTON_RIGHT_DOWN = 'rbxasset://textures/ui/ButtonRightDown.png'
|
|
local BUTTON = 'rbxasset://textures/ui/SingleButton.png'
|
|
local BUTTON_DOWN = 'rbxasset://textures/ui/SingleButtonDown.png'
|
|
local ROBUX_ICON = 'rbxasset://textures/ui/RobuxIcon.png'
|
|
local TIX_ICON = 'rbxasset://textures/ui/TixIcon.png'
|
|
local ERROR_ICON = 'rbxasset://textures/ui/ErrorIcon.png'
|
|
local A_BUTTON = "rbxasset://textures/ui/Settings/Help/AButtonDark.png"
|
|
local B_BUTTON = "rbxasset://textures/ui/Settings/Help/BButtonDark.png"
|
|
local DEFAULT_XBOX_IMAGE = 'rbxasset://textures/ui/Shell/Icons/ROBUXIcon@1080.png'
|
|
--Context Actions
|
|
local CONTROLLER_CONFIRM_ACTION_NAME = "CoreScriptPurchasePromptControllerConfirm"
|
|
local CONTROLLER_CANCEL_ACTION_NAME = "CoreScriptPurchasePromptControllerCancel"
|
|
local GAMEPAD_BUTTONS = {}
|
|
|
|
local ERROR_MSG = {
|
|
PURCHASE_DISABLED = "In-game purchases are temporarily disabled",
|
|
INVALID_FUNDS = "your account does not have enough ROBUX",
|
|
UNKNOWN = "ROBLOX is performing maintenance",
|
|
UNKNWON_FAILURE = "something went wrong"
|
|
}
|
|
local PURCHASE_MSG = {
|
|
SUCCEEDED = "Your purchase of itemName succeeded!",
|
|
FAILED = "Your purchase of itemName failed because errorReason. Your account has not been charged. Please try again later.",
|
|
PURCHASE = "Want to buy the assetType\nitemName for",
|
|
PURCHASE_TIX = "Want to buy the assetType\nitemName for",
|
|
FREE = "Would you like to take the assetType itemName for FREE?",
|
|
FREE_BALANCE = "Your account balance will not be affected by this transaction.",
|
|
BALANCE_FUTURE = "Your balance after this transaction will be ",
|
|
BALANCE_NOW = "Your balance is now ",
|
|
ALREADY_OWN = "You already own this item. Your account has not been charged."
|
|
}
|
|
local PURCHASE_FAILED = {
|
|
DEFAULT_ERROR = 0,
|
|
IN_GAME_PURCHASE_DISABLED = 1,
|
|
CANNOT_GET_BALANCE = 2,
|
|
CANNOT_GET_ITEM_PRICE = 3,
|
|
NOT_FOR_SALE = 4,
|
|
NOT_ENOUGH_TIX = 5,
|
|
UNDER_13 = 6,
|
|
LIMITED = 7,
|
|
DID_NOT_BUY_ROBUX = 8,
|
|
PROMPT_PURCHASE_ON_GUEST = 9,
|
|
THIRD_PARTY_DISABLED = 10,
|
|
}
|
|
local PURCHASE_STATE = {
|
|
DEFAULT = 1,
|
|
FAILED = 2,
|
|
SUCCEEDED = 3,
|
|
BUYITEM = 4,
|
|
BUYROBUX = 5,
|
|
BUYINGROBUX = 6,
|
|
BUYBC = 7
|
|
}
|
|
local BC_LVL_TO_STRING = {
|
|
"Builders Club",
|
|
"Turbo Builders Club",
|
|
"Outrageous Builders Club",
|
|
}
|
|
local ASSET_TO_STRING = {
|
|
[1] = "Image";
|
|
[2] = "T-Shirt";
|
|
[3] = "Audio";
|
|
[4] = "Mesh";
|
|
[5] = "Lua";
|
|
[6] = "HTML";
|
|
[7] = "Text";
|
|
[8] = "Hat";
|
|
[9] = "Place";
|
|
[10] = "Model";
|
|
[11] = "Shirt";
|
|
[12] = "Pants";
|
|
[13] = "Decal";
|
|
[16] = "Avatar";
|
|
[17] = "Head";
|
|
[18] = "Face";
|
|
[19] = "Gear";
|
|
[21] = "Badge";
|
|
[22] = "Group Emblem";
|
|
[24] = "Animation";
|
|
[25] = "Arms";
|
|
[26] = "Legs";
|
|
[27] = "Torso";
|
|
[28] = "Right Arm";
|
|
[29] = "Left Arm";
|
|
[30] = "Left Leg";
|
|
[31] = "Right Leg";
|
|
[32] = "Package";
|
|
[33] = "YouTube Video";
|
|
-- NOTE: GamePass and Plugin AssetTypeIds are different on ST1, ST2 and ST3
|
|
[34] = "Game Pass";
|
|
[38] = "Plugin";
|
|
[0] = "Product";
|
|
}
|
|
local BC_ROBUX_PRODUCTS = { 90, 180, 270, 360, 450, 1000, 2750 }
|
|
local NON_BC_ROBUX_PRODUCTS = { 80, 160, 240, 320, 400, 800, 2000 }
|
|
|
|
local DIALOG_SIZE = UDim2.new(0, 324, 0, 180)
|
|
local DIALOG_SIZE_TENFOOT = UDim2.new(0, 324*scaleFactor, 0, 180*scaleFactor)
|
|
local SHOW_POSITION = UDim2.new(0.5, -162, 0.5, -90)
|
|
local SHOW_POSITION_TENFOOT = UDim2.new(0.5, -162*scaleFactor, 0.5, -90*scaleFactor)
|
|
local HIDE_POSITION = UDim2.new(0.5, -162, 0, -181)
|
|
local HIDE_POSITION_TENFOOT = UDim2.new(0.5, -162*scaleFactor, 0, -180*scaleFactor - 1)
|
|
local BTN_SIZE = UDim2.new(0, 162, 0, 44)
|
|
local BTN_SIZE_TENFOOT = UDim2.new(0, 162*scaleFactor, 0, 44*scaleFactor)
|
|
local BODY_SIZE = UDim2.new(0, 324, 0, 136)
|
|
local BODY_SIZE_TENFOOT = UDim2.new(0, 324*scaleFactor, 0, 136*scaleFactor)
|
|
local TWEEN_TIME = 0.3
|
|
|
|
local BTN_L_POS = UDim2.new(0, 0, 0, 136)
|
|
local BTN_L_POS_TENFOOT = UDim2.new(0, 0, 0, 136*scaleFactor)
|
|
local BTN_R_POS = UDim2.new(0.5, 0, 0, 136)
|
|
local BTN_R_POS_TENFOOT = UDim2.new(0.5, 0, 0, 136*scaleFactor)
|
|
|
|
--[[ Utility Functions ]]--
|
|
local function lerp( start, finish, t)
|
|
return (1 - t) * start + t * finish
|
|
end
|
|
|
|
local function formatNumber(value)
|
|
return tostring(value):reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "")
|
|
end
|
|
|
|
--[[ Gui Creation Functions ]]--
|
|
local function createFrame(name, size, position, bgTransparency, bgColor)
|
|
local frame = Instance.new('Frame')
|
|
frame.Name = name
|
|
frame.Size = size
|
|
frame.Position = position or UDim2.new(0, 0, 0, 0)
|
|
frame.BackgroundTransparency = bgTransparency
|
|
frame.BackgroundColor3 = bgColor or Color3.new()
|
|
frame.BorderSizePixel = 0
|
|
frame.ZIndex = 8
|
|
|
|
return frame
|
|
end
|
|
|
|
local function createTextLabel(name, size, position, font, fontSize, text)
|
|
local textLabel = Instance.new('TextLabel')
|
|
textLabel.Name = name
|
|
textLabel.Size = size or UDim2.new(0, 0, 0, 0)
|
|
textLabel.Position = position
|
|
textLabel.BackgroundTransparency = 1
|
|
textLabel.Font = font
|
|
textLabel.FontSize = fontSize
|
|
textLabel.TextColor3 = Color3.new(1, 1, 1)
|
|
textLabel.Text = text
|
|
textLabel.ZIndex = 8
|
|
|
|
return textLabel
|
|
end
|
|
|
|
local function createImageLabel(name, size, position, image)
|
|
local imageLabel = Instance.new('ImageLabel')
|
|
imageLabel.Name = name
|
|
imageLabel.Size = size
|
|
imageLabel.BackgroundTransparency = 1
|
|
imageLabel.Position = position
|
|
imageLabel.Image = image
|
|
|
|
return imageLabel
|
|
end
|
|
|
|
local function createImageButtonWithText(name, position, image, imageDown, text, font)
|
|
local imageButton = Instance.new('ImageButton')
|
|
imageButton.Name = name
|
|
imageButton.Size = isTenFootInterface and BTN_SIZE_TENFOOT or BTN_SIZE
|
|
imageButton.Position = position
|
|
imageButton.Image = image
|
|
imageButton.BackgroundTransparency = 1
|
|
imageButton.AutoButtonColor = false
|
|
imageButton.ZIndex = 8
|
|
imageButton.Modal = true
|
|
|
|
local textLabel = createTextLabel(name.."Text", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 0), font, isTenFootInterface and largeFont or Enum.FontSize.Size24, text)
|
|
textLabel.ZIndex = 9
|
|
textLabel.Parent = imageButton
|
|
|
|
imageButton.MouseEnter:connect(function()
|
|
imageButton.Image = imageDown
|
|
end)
|
|
imageButton.MouseLeave:connect(function()
|
|
imageButton.Image = image
|
|
end)
|
|
imageButton.MouseButton1Click:connect(function()
|
|
imageButton.Image = image
|
|
end)
|
|
|
|
return imageButton
|
|
end
|
|
|
|
--[[ Begin Gui Creation ]]--
|
|
local PurchaseDialog = isTenFootInterface and createFrame("PurchaseDialog", DIALOG_SIZE_TENFOOT, HIDE_POSITION_TENFOOT, 1, nil) or createFrame("PurchaseDialog", DIALOG_SIZE, HIDE_POSITION, 1, nil)
|
|
PurchaseDialog.Visible = false
|
|
PurchaseDialog.Parent = RobloxGui
|
|
|
|
local ContainerFrame = createFrame("ContainerFrame", UDim2.new(1, 0, 1, 0), nil, 1, nil)
|
|
ContainerFrame.Parent = PurchaseDialog
|
|
|
|
local ContainerImage = createImageLabel("ContainerImage", isTenFootInterface and BODY_SIZE_TENFOOT or BODY_SIZE, UDim2.new(0, 0, 0, 0), BG_IMAGE)
|
|
ContainerImage.ZIndex = 8
|
|
ContainerImage.Parent = ContainerFrame
|
|
|
|
local ItemPreviewImage = isTenFootInterface and createImageLabel("ItemPreviewImage", UDim2.new(0, 64*scaleFactor, 0, 64*scaleFactor), UDim2.new(0, 27*scaleFactor, 0, 20*scaleFactor), "") or createImageLabel("ItemPreviewImage", UDim2.new(0, 64, 0, 64), UDim2.new(0, 27, 0, 20), "")
|
|
ItemPreviewImage.ZIndex = 9
|
|
ItemPreviewImage.Parent = ContainerFrame
|
|
|
|
local ItemDescriptionText = createTextLabel("ItemDescriptionText", isTenFootInterface and UDim2.new(0, 210*scaleFactor - 20, 0, 96*scaleFactor) or UDim2.new(0, 210, 0, 96), isTenFootInterface and UDim2.new(0, 110*scaleFactor, 0, 18*scaleFactor) or UDim2.new(0, 110, 0, 18),
|
|
Enum.Font.SourceSans, isTenFootInterface and Enum.FontSize.Size48 or Enum.FontSize.Size18, PURCHASE_MSG.PURCHASE)
|
|
ItemDescriptionText.TextXAlignment = Enum.TextXAlignment.Left
|
|
ItemDescriptionText.TextYAlignment = Enum.TextYAlignment.Top
|
|
ItemDescriptionText.TextWrapped = true
|
|
ItemDescriptionText.Parent = ContainerFrame
|
|
|
|
local RobuxIcon = createImageLabel("RobuxIcon", isTenFootInterface and UDim2.new(0, 20*scaleFactor, 0, 20*scaleFactor) or UDim2.new(0, 20, 0, 20), UDim2.new(0, 0, 0, 0), ROBUX_ICON)
|
|
RobuxIcon.ZIndex = 9
|
|
RobuxIcon.Visible = false
|
|
RobuxIcon.Parent = ContainerFrame
|
|
|
|
local TixIcon = createImageLabel("TixIcon", isTenFootInterface and UDim2.new(0, 20*scaleFactor, 0, 20*scaleFactor) or UDim2.new(0, 20, 0, 20), UDim2.new(0, 0, 0, 0), TIX_ICON)
|
|
TixIcon.ZIndex = 9
|
|
TixIcon.Visible = false
|
|
TixIcon.Parent = ContainerFrame
|
|
|
|
local CostText = createTextLabel("CostText", UDim2.new(0, 0, 0, 0), UDim2.new(0, 0, 0, 0),
|
|
Enum.Font.SourceSansBold, isTenFootInterface and largeFont or Enum.FontSize.Size18, "")
|
|
CostText.TextXAlignment = Enum.TextXAlignment.Left
|
|
CostText.Visible = false
|
|
CostText.Parent = ContainerFrame
|
|
|
|
local PostBalanceText = createTextLabel("PostBalanceText", UDim2.new(1, -20, 0, 30), isTenFootInterface and UDim2.new(0, 10, 0, 100*scaleFactor) or UDim2.new(0, 10, 0, 100), Enum.Font.SourceSans,
|
|
isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size14, "")
|
|
PostBalanceText.TextWrapped = true
|
|
PostBalanceText.Parent = ContainerFrame
|
|
|
|
local BuyButton = createImageButtonWithText("BuyButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, "Buy Now", Enum.Font.SourceSansBold)
|
|
BuyButton.Parent = ContainerFrame
|
|
local BuyButtonText = BuyButton:FindFirstChild("BuyButtonText")
|
|
|
|
local gamepadButtonXLocation = (BuyButton.AbsoluteSize.X/2 - BuyButtonText.TextBounds.X/2)/2
|
|
local buyButtonGamepadImage = Instance.new("ImageLabel")
|
|
buyButtonGamepadImage.BackgroundTransparency = 1
|
|
buyButtonGamepadImage.Image = A_BUTTON
|
|
buyButtonGamepadImage.Size = UDim2.new(1, -8, 1, -8)
|
|
buyButtonGamepadImage.SizeConstraint = Enum.SizeConstraint.RelativeYY
|
|
buyButtonGamepadImage.Parent = BuyButton
|
|
buyButtonGamepadImage.Position = UDim2.new(0, gamepadButtonXLocation - buyButtonGamepadImage.AbsoluteSize.X/2, 0, 5)
|
|
buyButtonGamepadImage.Visible = false
|
|
buyButtonGamepadImage.ZIndex = BuyButton.ZIndex
|
|
table.insert(GAMEPAD_BUTTONS, buyButtonGamepadImage)
|
|
|
|
local CancelButton = createImageButtonWithText("CancelButton", isTenFootInterface and BTN_R_POS_TENFOOT or BTN_R_POS, BUTTON_RIGHT, BUTTON_RIGHT_DOWN, "Cancel", Enum.Font.SourceSans)
|
|
CancelButton.Parent = ContainerFrame
|
|
|
|
local cancelButtonGamepadImage = buyButtonGamepadImage:Clone()
|
|
cancelButtonGamepadImage.Image = B_BUTTON
|
|
cancelButtonGamepadImage.ZIndex = CancelButton.ZIndex
|
|
cancelButtonGamepadImage.Parent = CancelButton
|
|
table.insert(GAMEPAD_BUTTONS, cancelButtonGamepadImage)
|
|
|
|
local BuyRobuxButton = createImageButtonWithText("BuyRobuxButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, IsNativePurchasing and "Buy" or "Buy R$",
|
|
Enum.Font.SourceSansBold)
|
|
BuyRobuxButton.Visible = false
|
|
BuyRobuxButton.Parent = ContainerFrame
|
|
|
|
local buyRobuxGamepadImage = buyButtonGamepadImage:Clone()
|
|
buyRobuxGamepadImage.ZIndex = BuyRobuxButton.ZIndex
|
|
buyRobuxGamepadImage.Parent = BuyRobuxButton
|
|
table.insert(GAMEPAD_BUTTONS, buyRobuxGamepadImage)
|
|
|
|
local BuyBCButton = createImageButtonWithText("BuyBCButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, "Upgrade", Enum.Font.SourceSansBold)
|
|
BuyBCButton.Visible = false
|
|
BuyBCButton.Parent = ContainerFrame
|
|
|
|
local buyBCGamepadImage = buyButtonGamepadImage:Clone()
|
|
buyBCGamepadImage.ZIndex = BuyBCButton.ZIndex
|
|
buyBCGamepadImage.Parent = BuyBCButton
|
|
table.insert(GAMEPAD_BUTTONS, buyBCGamepadImage)
|
|
|
|
local FreeButton = createImageButtonWithText("FreeButton", isTenFootInterface and BTN_L_POS_TENFOOT or BTN_L_POS, BUTTON_LEFT, BUTTON_LEFT_DOWN, "Take Free", Enum.Font.SourceSansBold)
|
|
FreeButton.Visible = false
|
|
FreeButton.Parent = ContainerFrame
|
|
|
|
local OkButton = createImageButtonWithText("OkButton", isTenFootInterface and UDim2.new(0, 2, 0, 136*scaleFactor) or UDim2.new(0, 2, 0, 136), BUTTON, BUTTON_DOWN, "OK", Enum.Font.SourceSans)
|
|
OkButton.Size = isTenFootInterface and UDim2.new(0, 320*scaleFactor, 0, 44*scaleFactor) or UDim2.new(0, 320, 0, 44)
|
|
OkButton.Visible = false
|
|
OkButton.Parent = ContainerFrame
|
|
|
|
local okButtonGamepadImage = buyButtonGamepadImage:Clone()
|
|
okButtonGamepadImage.ZIndex = OkButton.ZIndex
|
|
okButtonGamepadImage.Parent = OkButton
|
|
table.insert(GAMEPAD_BUTTONS, okButtonGamepadImage)
|
|
|
|
local OkPurchasedButton = createImageButtonWithText("OkPurchasedButton", isTenFootInterface and UDim2.new(0, 2, 0, 136*scaleFactor) or UDim2.new(0, 2, 0, 136), BUTTON, BUTTON_DOWN, "OK", Enum.Font.SourceSans)
|
|
OkPurchasedButton.Size = isTenFootInterface and UDim2.new(0, 320*scaleFactor, 0, 44*scaleFactor) or UDim2.new(0, 320, 0, 44)
|
|
OkPurchasedButton.Visible = false
|
|
OkPurchasedButton.Parent = ContainerFrame
|
|
|
|
local okPurchasedGamepadImage = buyButtonGamepadImage:Clone()
|
|
okPurchasedGamepadImage.ZIndex = OkPurchasedButton.ZIndex
|
|
okPurchasedGamepadImage.Parent = OkPurchasedButton
|
|
table.insert(GAMEPAD_BUTTONS, okPurchasedGamepadImage)
|
|
|
|
local PurchaseFrame = createImageLabel("PurchaseFrame", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 0), PURCHASE_BG)
|
|
PurchaseFrame.ZIndex = 8
|
|
PurchaseFrame.Visible = false
|
|
PurchaseFrame.Parent = PurchaseDialog
|
|
|
|
local PurchaseText = createTextLabel("PurchaseText", nil, UDim2.new(0.5, 0, 0.5, -36), Enum.Font.SourceSans,
|
|
isTenFootInterface and largeFont or Enum.FontSize.Size36, "Purchasing")
|
|
PurchaseText.Parent = PurchaseFrame
|
|
|
|
local LoadingFrames = {}
|
|
local xOffset = -40
|
|
for i = 1, 3 do
|
|
local frame = createFrame("Loading", UDim2.new(0, 16, 0, 16), UDim2.new(0.5, xOffset, 0.5, 0), 0, Color3.new(132/255, 132/255, 132/255))
|
|
table.insert(LoadingFrames, frame)
|
|
frame.Parent = PurchaseFrame
|
|
xOffset = xOffset + 32
|
|
end
|
|
|
|
|
|
local function noOpFunc() end
|
|
|
|
local function enableControllerMovement()
|
|
game:GetService("ContextActionService"):UnbindCoreAction(freezeThumbstick1Name)
|
|
game:GetService("ContextActionService"):UnbindCoreAction(freezeThumbstick2Name)
|
|
game:GetService("ContextActionService"):UnbindCoreAction(freezeControllerActionName)
|
|
end
|
|
|
|
local function disableControllerMovement()
|
|
game:GetService("ContextActionService"):BindCoreAction(freezeControllerActionName, noOpFunc, false, Enum.UserInputType.Gamepad1)
|
|
game:GetService("ContextActionService"):BindCoreAction(freezeThumbstick1Name, noOpFunc, false, Enum.KeyCode.Thumbstick1)
|
|
game:GetService("ContextActionService"):BindCoreAction(freezeThumbstick2Name, noOpFunc, false, Enum.KeyCode.Thumbstick2)
|
|
end
|
|
|
|
--[[ Purchase Data Functions ]]--
|
|
local function getCurrencyString(currencyType)
|
|
return currencyType == Enum.CurrencyType.Tix and "Tix" or "R$"
|
|
end
|
|
|
|
local function setInitialPurchaseData(assetId, productId, currencyType, equipOnPurchase)
|
|
PurchaseData.AssetId = assetId
|
|
PurchaseData.ProductId = productId
|
|
PurchaseData.CurrencyType = currencyType
|
|
PurchaseData.EquipOnPurchase = equipOnPurchase
|
|
|
|
IsPurchasingConsumable = productId ~= nil
|
|
end
|
|
|
|
local function setCurrencyData(playerBalance)
|
|
local priceInRobux = tonumber(PurchaseData.ProductInfo['PriceInRobux'])
|
|
local priceInTickets = tonumber(PurchaseData.ProductInfo['PriceInTickets'])
|
|
--
|
|
if PurchaseData.CurrencyType == Enum.CurrencyType.Default or PurchaseData.CurrencyType == Enum.CurrencyType.Robux then
|
|
if priceInRobux and priceInRobux ~= 0 then
|
|
PurchaseData.CurrencyAmount = priceInRobux
|
|
PurchaseData.CurrencyType = Enum.CurrencyType.Robux
|
|
else
|
|
PurchaseData.CurrencyAmount = priceInTickets
|
|
PurchaseData.CurrencyType = Enum.CurrencyType.Tix
|
|
end
|
|
elseif PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
if priceInTickets and priceInTickets ~= 0 then
|
|
PurchaseData.CurrencyAmount = priceInTickets
|
|
else
|
|
PurchaseData.CurrencyAmount = priceInRobux
|
|
PurchaseData.CurrencyType = Enum.CurrencyType.Robux
|
|
end
|
|
end
|
|
end
|
|
|
|
local function setPreviewImageXbox(productInfo, assetId)
|
|
-- get the asset id we want
|
|
local id = nil
|
|
if IsPurchasingConsumable and productInfo and productInfo["IconImageAssetId"] then
|
|
id = productInfo["IconImageAssetId"]
|
|
elseif assetId then
|
|
id = assetId
|
|
else
|
|
ItemPreviewImage.Image = DEFAULT_XBOX_IMAGE
|
|
return
|
|
end
|
|
|
|
local path = 'asset-thumbnail/json?assetId=%d&width=100&height=100&format=png'
|
|
path = BASE_URL..string.format(path, id)
|
|
spawn(function()
|
|
-- check if thumb has been generated, if not generated or if anything fails
|
|
-- set to the default image
|
|
local success, result = pcall(function()
|
|
return game:HttpGetAsync(path)
|
|
end)
|
|
if not success then
|
|
ItemPreviewImage.Image = DEFAULT_XBOX_IMAGE
|
|
return
|
|
end
|
|
|
|
local decodeSuccess, decodeResult = pcall(function()
|
|
return HttpService:JSONDecode(result)
|
|
end)
|
|
if not decodeSuccess then
|
|
ItemPreviewImage.Image = DEFAULT_XBOX_IMAGE
|
|
return
|
|
end
|
|
|
|
if decodeResult["Final"] == true then
|
|
ItemPreviewImage.Image = THUMBNAIL_URL..tostring(id).."&x=100&y=100&format=png"
|
|
else
|
|
ItemPreviewImage.Image = DEFAULT_XBOX_IMAGE
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function setPreviewImage(productInfo, assetId)
|
|
-- For now let's only run this logic on Xbox
|
|
if platform == Enum.Platform.XBoxOne then
|
|
setPreviewImageXbox(productInfo, assetId)
|
|
return
|
|
end
|
|
if IsPurchasingConsumable then
|
|
if productInfo then
|
|
ItemPreviewImage.Image = THUMBNAIL_URL..tostring(productInfo["IconImageAssetId"].."&x=100&y=100&format=png")
|
|
end
|
|
else
|
|
if assetId then
|
|
ItemPreviewImage.Image = THUMBNAIL_URL..tostring(assetId).."&x=100&y=100&format=png"
|
|
end
|
|
end
|
|
end
|
|
|
|
local function clearPurchaseData()
|
|
for k,v in pairs(PurchaseData) do
|
|
PurchaseData[k] = nil
|
|
end
|
|
RobuxIcon.Visible = false
|
|
TixIcon.Visible = false
|
|
CostText.Visible = false
|
|
end
|
|
|
|
--[[ Show Functions ]]--
|
|
local function setButtonsVisible(...)
|
|
local args = {...}
|
|
local argCount = select('#', ...)
|
|
|
|
for _,child in pairs(ContainerFrame:GetChildren()) do
|
|
if child:IsA('ImageButton') then
|
|
child.Visible = false
|
|
for i = 1, argCount do
|
|
if child == args[i] then
|
|
child.Visible = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function tweenBackgroundColor(frame, endColor, duration)
|
|
local t = 0
|
|
local prevTime = tick()
|
|
local startColor = frame.BackgroundColor3
|
|
while t < duration do
|
|
local s = t / duration
|
|
local r = lerp(startColor.r, endColor.r, s)
|
|
local g = lerp(startColor.g, endColor.g, s)
|
|
local b = lerp(startColor.b, endColor.b, s)
|
|
frame.BackgroundColor3 = Color3.new(r, g, b)
|
|
--
|
|
t = t + (tick() - prevTime)
|
|
prevTime = tick()
|
|
wait()
|
|
end
|
|
frame.BackgroundColor3 = endColor
|
|
end
|
|
|
|
local isPurchaseAnimating = false
|
|
local function startPurchaseAnimation()
|
|
if PurchaseFrame.Visible then return end
|
|
--
|
|
ContainerFrame.Visible = false
|
|
PurchaseFrame.Visible = true
|
|
--
|
|
spawn(function()
|
|
isPurchaseAnimating = true
|
|
local i = 1
|
|
while isPurchaseAnimating do
|
|
local frame = LoadingFrames[i]
|
|
local prevPosition = frame.Position
|
|
local newPosition = UDim2.new(prevPosition.X.Scale, prevPosition.X.Offset, prevPosition.Y.Scale, prevPosition.Y.Offset - 2)
|
|
spawn(function()
|
|
tweenBackgroundColor(frame, Color3.new(0, 162/255, 1), 0.25)
|
|
end)
|
|
frame:TweenSizeAndPosition(UDim2.new(0, 16, 0, 20), newPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true, function()
|
|
spawn(function()
|
|
tweenBackgroundColor(frame, Color3.new(132/255, 132/255, 132/255), 0.25)
|
|
end)
|
|
frame:TweenSizeAndPosition(UDim2.new(0, 16, 0, 16), prevPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, 0.25, true)
|
|
end)
|
|
i = i + 1
|
|
if i > 3 then
|
|
i = 1
|
|
wait(0.25) -- small pause when starting from 1
|
|
end
|
|
wait(0.5)
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function stopPurchaseAnimation()
|
|
isPurchaseAnimating = false
|
|
PurchaseFrame.Visible = false
|
|
ContainerFrame.Visible = true
|
|
end
|
|
|
|
local function setPurchaseDataInGui(isFree, invalidBC)
|
|
local descriptionText = PurchaseData.CurrencyType == Enum.CurrencyType.Tix and PURCHASE_MSG.PURCHASE_TIX or PURCHASE_MSG.PURCHASE
|
|
if isFree then
|
|
descriptionText = PURCHASE_MSG.FREE
|
|
PostBalanceText.Text = PURCHASE_MSG.FREE_BALANCE
|
|
end
|
|
|
|
local productInfo = PurchaseData.ProductInfo
|
|
if not productInfo then
|
|
return false
|
|
end
|
|
local itemDescription = string.gsub(descriptionText, "itemName", string.sub(productInfo["Name"], 1, 20))
|
|
itemDescription = string.gsub(itemDescription, "assetType", ASSET_TO_STRING[productInfo["AssetTypeId"]] or "Unknown")
|
|
ItemDescriptionText.Text = itemDescription
|
|
|
|
if not isFree then
|
|
if PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
TixIcon.Visible = true
|
|
TixIcon.Position = UDim2.new(0, isTenFootInterface and 110*scaleFactor or 110, 0, ItemDescriptionText.Position.Y.Offset + ItemDescriptionText.TextBounds.y + (isTenFootInterface and 6*scaleFactor or 6))
|
|
CostText.TextColor3 = Color3.new(204/255, 158/255, 113/255)
|
|
else
|
|
RobuxIcon.Visible = true
|
|
RobuxIcon.Position = UDim2.new(0, isTenFootInterface and 110*scaleFactor or 110, 0, ItemDescriptionText.Position.Y.Offset + ItemDescriptionText.TextBounds.y + (isTenFootInterface and 6*scaleFactor or 6))
|
|
CostText.TextColor3 = Color3.new(2/255, 183/255, 87/255)
|
|
end
|
|
CostText.Text = formatNumber(PurchaseData.CurrencyAmount)
|
|
CostText.Position = UDim2.new(0, isTenFootInterface and 134*scaleFactor or 134, 0, ItemDescriptionText.Position.Y.Offset + ItemDescriptionText.TextBounds.y + (isTenFootInterface and 15*scaleFactor or 15))
|
|
CostText.Visible = true
|
|
end
|
|
|
|
setPreviewImage(productInfo, PurchaseData.AssetId)
|
|
purchaseState = PURCHASE_STATE.BUYITEM
|
|
setButtonsVisible(isFree and FreeButton or BuyButton, CancelButton)
|
|
PostBalanceText.Visible = true
|
|
|
|
if invalidBC then
|
|
local neededBcLevel = PurchaseData.ProductInfo["MinimumMembershipLevel"]
|
|
PostBalanceText.Text = "This item requires "..BC_LVL_TO_STRING[neededBcLevel]..".\nClick 'Upgrade' to upgrade your Builders Club!"
|
|
purchaseState = PURCHASE_STATE.BUYBC
|
|
setButtonsVisible(BuyBCButton, CancelButton)
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function getRobuxProduct(amountNeeded, isBCMember)
|
|
local productArray = nil
|
|
|
|
if platform == Enum.Platform.XBoxOne then
|
|
productArray = {}
|
|
local platformCatalogData = require(RobloxGui.Modules.PlatformCatalogData)
|
|
|
|
local catalogInfo = platformCatalogData:GetCatalogInfoAsync()
|
|
if catalogInfo then
|
|
for _, productInfo in pairs(catalogInfo) do
|
|
local robuxValue = platformCatalogData:ParseRobuxValue(productInfo)
|
|
table.insert(productArray, robuxValue)
|
|
end
|
|
end
|
|
else
|
|
productArray = isBCMember and BC_ROBUX_PRODUCTS or NON_BC_ROBUX_PRODUCTS
|
|
end
|
|
|
|
table.sort(productArray, function(a,b) return a < b end)
|
|
|
|
for i = 1, #productArray do
|
|
if productArray[i] >= amountNeeded then
|
|
return productArray[i]
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function getRobuxProductToBuyItem(amountNeeded)
|
|
local isBCMember = Players.LocalPlayer.MembershipType ~= Enum.MembershipType.None
|
|
|
|
local productCost = getRobuxProduct(amountNeeded, isBCMember)
|
|
if not productCost then
|
|
return nil
|
|
end
|
|
|
|
--todo: we should clean all this up at some point so all the platforms have the
|
|
-- same product names, or at least names that are very similar
|
|
|
|
local isUsingNewProductId = (platform == Enum.Platform.Android) or (platform == Enum.Platform.UWP)
|
|
|
|
local prependStr, appendStr, appPrefix = "", "", ""
|
|
if isUsingNewProductId then
|
|
prependStr = "robux"
|
|
if isBCMember then
|
|
appendStr = "bc"
|
|
end
|
|
appPrefix = "wtf.watrbx.client."
|
|
elseif platform == Enum.Platform.XBoxOne then
|
|
local platformCatalogData = require(RobloxGui.Modules.PlatformCatalogData)
|
|
|
|
local catalogInfo = platformCatalogData:GetCatalogInfoAsync()
|
|
if catalogInfo then
|
|
for _, productInfo in pairs(catalogInfo) do
|
|
if platformCatalogData:ParseRobuxValue(productInfo) == productCost then
|
|
return productInfo.ProductId, productCost
|
|
end
|
|
end
|
|
end
|
|
else -- used by iOS
|
|
appendStr = isBCMember and "RobuxBC" or "RobuxNonBC"
|
|
appPrefix = "wtf.watrbx.robloxmobile."
|
|
end
|
|
|
|
local productStr = appPrefix..prependStr..tostring(productCost)..appendStr
|
|
return productStr, productCost
|
|
end
|
|
|
|
local function setBuyMoreRobuxDialog(playerBalance)
|
|
local playerBalanceInt = tonumber(playerBalance["robux"])
|
|
local neededRobux = PurchaseData.CurrencyAmount - playerBalanceInt
|
|
local productInfo = PurchaseData.ProductInfo
|
|
|
|
local descriptionText = "You need %s more ROBUX to buy the %s %s"
|
|
descriptionText = string.format(descriptionText, formatNumber(neededRobux), productInfo["Name"], ASSET_TO_STRING[productInfo["AssetTypeId"]] or "")
|
|
|
|
purchaseState = PURCHASE_STATE.BUYROBUX
|
|
setButtonsVisible(BuyRobuxButton, CancelButton)
|
|
|
|
if IsNativePurchasing then
|
|
local productCost = nil
|
|
ThirdPartyProductName, productCost = getRobuxProductToBuyItem(neededRobux)
|
|
--
|
|
if not ThirdPartyProductName then
|
|
descriptionText = "This item cost more ROBUX than you can purchase. Please visit www.watrbx.wtf to purchase more ROBUX."
|
|
purchaseState = PURCHASE_STATE.FAILED
|
|
setButtonsVisible(OkButton)
|
|
else
|
|
local remainder = playerBalanceInt + productCost - PurchaseData.CurrencyAmount
|
|
descriptionText = descriptionText..". Would you like to buy "..formatNumber(productCost).." ROBUX?"
|
|
PostBalanceText.Text = "The remaining "..formatNumber(remainder).." ROBUX will be credited to your balance."
|
|
PostBalanceText.Visible = true
|
|
end
|
|
else
|
|
descriptionText = descriptionText..". Would you like to buy more ROBUX?"
|
|
end
|
|
ItemDescriptionText.Text = descriptionText
|
|
setPreviewImage(productInfo, PurchaseData.AssetId)
|
|
end
|
|
|
|
local function showPurchasePrompt()
|
|
stopPurchaseAnimation()
|
|
PurchaseDialog.Visible = true
|
|
if isTenFootInterface then
|
|
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
|
|
end
|
|
PurchaseDialog:TweenPosition(isTenFootInterface and SHOW_POSITION_TENFOOT or SHOW_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true)
|
|
disableControllerMovement()
|
|
enableControllerInput()
|
|
end
|
|
|
|
--[[ Close and Cancel Functions ]]--
|
|
local function onPurchaseFailed(failType)
|
|
setButtonsVisible(OkButton)
|
|
ItemPreviewImage.Image = ERROR_ICON
|
|
PostBalanceText.Text = ""
|
|
|
|
local itemName = PurchaseData.ProductInfo and PurchaseData.ProductInfo["Name"] or ""
|
|
local failedText = string.gsub(PURCHASE_MSG.FAILED, "itemName", string.sub(itemName, 1, 20))
|
|
|
|
if itemName == "" then
|
|
failedText = string.gsub(failedText, " of ", "")
|
|
end
|
|
|
|
if failType == PURCHASE_FAILED.DEFAULT_ERROR then
|
|
failedText = string.gsub(failedText, "errorReason", ERROR_MSG.UNKNWON_FAILURE)
|
|
elseif failType == PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED then
|
|
failedText = string.gsub(failedText, "errorReason", ERROR_MSG.PURCHASE_DISABLED)
|
|
elseif failType == PURCHASE_FAILED.CANNOT_GET_BALANCE then
|
|
failedText = "Cannot retrieve your balance at this time. Your account has not been charged. Please try again later."
|
|
elseif failType == PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE then
|
|
failedText = "We couldn't retrieve the price of the item at this time. Your account has not been charged. Please try again later."
|
|
elseif failType == PURCHASE_FAILED.NOT_FOR_SALE then
|
|
failedText = "This item is not currently for sale. Your account has not been charged."
|
|
setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId)
|
|
elseif failType == PURCHASE_FAILED.NOT_ENOUGH_TIX then
|
|
failedText = "This item cost more tickets than you currently have. Try trading currency on www.watrbx.wtf to get more tickets."
|
|
setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId)
|
|
elseif failType == PURCHASE_FAILED.UNDER_13 then
|
|
failedText = "Your account is under 13. Purchase of this item is not allowed. Your account has not been charged."
|
|
elseif failType == PURCHASE_FAILED.LIMITED then
|
|
failedText = "This limited item has no more copies. Try buying from another user on www.watrbx.wtf. Your account has not been charged."
|
|
setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId)
|
|
elseif failType == PURCHASE_FAILED.DID_NOT_BUY_ROBUX then
|
|
failedText = string.gsub(failedText, "errorReason", ERROR_MSG.INVALID_FUNDS)
|
|
elseif failType == PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST then
|
|
failedText = "You need to create a ROBLOX account to buy items, visit www.watrbx.wtf for more info."
|
|
elseif failType == PURCHASE_FAILED.THIRD_PARTY_DISABLED then
|
|
failedText = "Third-party item sales have been disabled for this place. Your account has not been charged."
|
|
setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId)
|
|
end
|
|
|
|
RobuxIcon.Visible = false
|
|
TixIcon.Visible = false
|
|
CostText.Visible = false
|
|
|
|
purchaseState = PURCHASE_STATE.FAILED
|
|
|
|
ItemDescriptionText.Text = failedText
|
|
showPurchasePrompt()
|
|
end
|
|
|
|
local function closePurchaseDialog()
|
|
PurchaseDialog:TweenPosition(isTenFootInterface and HIDE_POSITION_TENFOOT or HIDE_POSITION, Enum.EasingDirection.InOut, Enum.EasingStyle.Quad, TWEEN_TIME, true, function()
|
|
PurchaseDialog.Visible = false
|
|
IsCurrentlyPrompting = false
|
|
IsCurrentlyPurchasing = false
|
|
IsCheckingPlayerFunds = false
|
|
purchaseState = PURCHASE_STATE.DEFAULT
|
|
if isTenFootInterface then
|
|
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Main exit point
|
|
local function onPromptEnded(isSuccess)
|
|
local didPurchase = (purchaseState == PURCHASE_STATE.SUCCEEDED)
|
|
|
|
closePurchaseDialog()
|
|
if IsPurchasingConsumable then
|
|
MarketplaceService:SignalPromptProductPurchaseFinished(Players.LocalPlayer.userId, PurchaseData.ProductId, didPurchase)
|
|
else
|
|
MarketplaceService:SignalPromptPurchaseFinished(Players.LocalPlayer, PurchaseData.AssetId, didPurchase)
|
|
end
|
|
clearPurchaseData()
|
|
enableControllerMovement()
|
|
disableControllerInput()
|
|
end
|
|
|
|
--[[ Purchase Validation ]]--
|
|
local function isMarketplaceDown() -- FFlag
|
|
local success, result = pcall(function() return settings():GetFFlag('Order66') end)
|
|
if not success then
|
|
print("PurchasePromptScript: isMarketplaceDown failed because", result)
|
|
return false
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function checkMarketplaceAvailable() -- FFlag
|
|
local success, result = pcall(function() return settings():GetFFlag("CheckMarketplaceAvailable") end)
|
|
if not success then
|
|
print("PurchasePromptScript: checkMarketplaceAvailable failed because", result)
|
|
return false
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function areThirdPartySalesRestricted() -- FFlag
|
|
local success, result = pcall(function() return settings():GetFFlag("RestrictSales") end)
|
|
if not success then
|
|
print("PurchasePromptScript: areThirdPartySalesRestricted failed because", result)
|
|
return false
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
-- return success and isAvailable
|
|
local function isMarketplaceAvailable()
|
|
local success, result = pcall(function()
|
|
return HttpRbxApiService:GetAsync("my/economy-status", false,
|
|
Enum.ThrottlingPriority.Extreme)
|
|
end)
|
|
if not success then
|
|
print("PurchasePromptScript: isMarketplaceAvailable() failed because", result)
|
|
return false
|
|
end
|
|
result = HttpService:JSONDecode(result)
|
|
if result["isMarketplaceEnabled"] ~= nil then
|
|
if result["isMarketplaceEnabled"] == false then
|
|
return true, false
|
|
end
|
|
end
|
|
return true, true
|
|
end
|
|
|
|
local function getProductInfo()
|
|
local success, result = nil, nil
|
|
if IsPurchasingConsumable then
|
|
success, result = pcall(function()
|
|
return MarketplaceService:GetProductInfo(PurchaseData.ProductId, Enum.InfoType.Product)
|
|
end)
|
|
else
|
|
success, result = pcall(function()
|
|
return MarketplaceService:GetProductInfo(PurchaseData.AssetId)
|
|
end)
|
|
end
|
|
|
|
if not success or not result then
|
|
print("PurchasePromptScript: getProductInfo failed because", result)
|
|
return nil
|
|
end
|
|
|
|
if type(result) ~= 'table' then
|
|
result = HttpService:JSONDecode(result)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
-- returns success, doesOwnItem
|
|
local function doesPlayerOwnItem()
|
|
if not PurchaseData.AssetId or PurchaseData.AssetId <= 0 then
|
|
return false, nil
|
|
end
|
|
|
|
local success, result = pcall(function()
|
|
local apiPath = "ownership/hasAsset"
|
|
local params = "?userId="..tostring(Players.LocalPlayer.userId).."&assetId="..tostring(PurchaseData.AssetId)
|
|
return HttpRbxApiService:GetAsync(apiPath..params, true)
|
|
end)
|
|
|
|
if not success then
|
|
print("PurchasePromptScript: doesPlayerOwnItem() failed because", result)
|
|
return false, nil
|
|
end
|
|
|
|
if result == true or result == "true" then
|
|
return true, true
|
|
end
|
|
|
|
return true, false
|
|
end
|
|
|
|
local function isFreeItem()
|
|
return PurchaseData.ProductInfo and PurchaseData.ProductInfo["IsPublicDomain"] == true
|
|
end
|
|
|
|
local function getPlayerBalance()
|
|
local apiPath = platform == Enum.Platform.XBoxOne and 'my/platform-currency-budget' or 'currency/balance'
|
|
|
|
local success, result = pcall(function()
|
|
return HttpRbxApiService:GetAsync(apiPath, true)
|
|
end)
|
|
|
|
if not success then
|
|
print("PurchasePromptScript: getPlayerBalance() failed because", result)
|
|
return nil
|
|
end
|
|
|
|
if result == '' then return end
|
|
|
|
result = HttpService:JSONDecode(result)
|
|
if platform == Enum.Platform.XBoxOne then
|
|
result["robux"] = result["Robux"]
|
|
result["tickets"] = "0"
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local function isNotForSale()
|
|
return PurchaseData.ProductInfo['IsForSale'] == false and PurchaseData.ProductInfo["IsPublicDomain"] == false
|
|
end
|
|
|
|
local function playerHasFundsForPurchase(playerBalance)
|
|
local currencyTypeStr = nil
|
|
if PurchaseData.CurrencyType == Enum.CurrencyType.Robux then
|
|
currencyTypeStr = "robux"
|
|
elseif PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
currencyTypeStr = "tickets"
|
|
else
|
|
return false
|
|
end
|
|
|
|
local playerBalanceInt = tonumber(playerBalance[currencyTypeStr])
|
|
if not playerBalanceInt then
|
|
return false
|
|
end
|
|
|
|
local afterBalanceAmount = playerBalanceInt - PurchaseData.CurrencyAmount
|
|
local currencyStr = getCurrencyString(PurchaseData.CurrencyType)
|
|
if afterBalanceAmount < 0 and PurchaseData.CurrencyType == Enum.CurrencyType.Robux then
|
|
PostBalanceText.Visible = false
|
|
return true, false
|
|
elseif afterBalanceAmount < 0 and PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
PostBalanceText.Visible = true
|
|
PostBalanceText.Text = "You need "..formatNumber(-afterBalanceAmount).." more "..currencyStr.." to buy this item."
|
|
return true, false
|
|
end
|
|
|
|
if PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
PostBalanceText.Text = PURCHASE_MSG.BALANCE_FUTURE..formatNumber(afterBalanceAmount).." "..currencyStr.."."
|
|
else
|
|
PostBalanceText.Text = PURCHASE_MSG.BALANCE_FUTURE..currencyStr..formatNumber(afterBalanceAmount).."."
|
|
end
|
|
|
|
return true, true
|
|
end
|
|
|
|
local function isUnder13()
|
|
if PurchaseData.ProductInfo["ContentRatingTypeId"] == 1 then
|
|
if Players.LocalPlayer:GetUnder13() then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function isLimitedUnique()
|
|
local productInfo = PurchaseData.ProductInfo
|
|
if productInfo then
|
|
if (productInfo["IsLimited"] or productInfo["IsLimitedUnique"]) and
|
|
(productInfo["Remaining"] == "" or productInfo["Remaining"] == 0 or productInfo["Remaining"] == nil or productInfo["Remaining"] == "null") then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- main validation function
|
|
local function canPurchase(disableUpsell)
|
|
if game.Players.LocalPlayer.userId < 0 then
|
|
onPurchaseFailed(PURCHASE_FAILED.PROMPT_PURCHASE_ON_GUEST)
|
|
return false
|
|
end
|
|
|
|
if isMarketplaceDown() then -- FFlag
|
|
onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED)
|
|
return false
|
|
end
|
|
|
|
if checkMarketplaceAvailable() then -- FFlag
|
|
local success, isAvailable = isMarketplaceAvailable()
|
|
if success then
|
|
if not isAvailable then
|
|
onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED)
|
|
return false
|
|
end
|
|
else
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return false
|
|
end
|
|
end
|
|
|
|
PurchaseData.ProductInfo = getProductInfo()
|
|
if not PurchaseData.ProductInfo then
|
|
onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED)
|
|
return false
|
|
end
|
|
|
|
if isNotForSale() then
|
|
onPurchaseFailed(PURCHASE_FAILED.NOT_FOR_SALE)
|
|
return false
|
|
end
|
|
|
|
-- check if owned by player; dev products are not owned
|
|
local isRestrictedThirdParty = false
|
|
if not IsPurchasingConsumable then
|
|
local success, doesOwnItem = doesPlayerOwnItem()
|
|
if not success then
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return false
|
|
elseif doesOwnItem then
|
|
if not PurchaseData.ProductInfo then
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return false
|
|
end
|
|
purchaseState = PURCHASE_STATE.FAILED
|
|
setPreviewImage(PurchaseData.ProductInfo, PurchaseData.AssetId)
|
|
ItemDescriptionText.Text = PURCHASE_MSG.ALREADY_OWN
|
|
PostBalanceText.Visible = false
|
|
setButtonsVisible(OkButton)
|
|
return true
|
|
end
|
|
|
|
-- most places will not need to sell third party assets.
|
|
if areThirdPartySalesRestricted() and not game:GetService("Workspace").AllowThirdPartySales then
|
|
local ProductCreator = tonumber(PurchaseData.ProductInfo["Creator"]["Id"])
|
|
local RobloxCreator = 1
|
|
if ProductCreator ~= game.CreatorId and ProductCreator ~= RobloxCreator then
|
|
isRestrictedThirdParty = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local isFree = isFreeItem()
|
|
|
|
if not isFree and isRestrictedThirdParty then
|
|
onPurchaseFailed(PURCHASE_FAILED.THIRD_PARTY_DISABLED)
|
|
return false
|
|
end
|
|
|
|
local playerBalance = getPlayerBalance()
|
|
if not playerBalance then
|
|
onPurchaseFailed(PURCHASE_FAILED.CANNOT_GET_BALANCE)
|
|
return false
|
|
end
|
|
|
|
-- validate item price
|
|
setCurrencyData(playerBalance)
|
|
if not PurchaseData.CurrencyAmount and not isFree then
|
|
onPurchaseFailed(PURCHASE_FAILED.CANNOT_GET_ITEM_PRICE)
|
|
return false
|
|
end
|
|
|
|
-- check player funds
|
|
local hasFunds = nil
|
|
if not isFree then
|
|
local success = nil
|
|
success, hasFunds = playerHasFundsForPurchase(playerBalance)
|
|
if success then
|
|
if not hasFunds then
|
|
if PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
onPurchaseFailed(PURCHASE_FAILED.NOT_ENOUGH_TIX)
|
|
return false
|
|
elseif not disableUpsell then
|
|
setBuyMoreRobuxDialog(playerBalance)
|
|
end
|
|
end
|
|
else
|
|
onPurchaseFailed(PURCHASE_FAILED.CANNOT_GET_BALANCE)
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- check membership type
|
|
local invalidBCLevel = PurchaseData.ProductInfo["MinimumMembershipLevel"] > Players.LocalPlayer.MembershipType.Value
|
|
|
|
-- check under 13
|
|
if isUnder13() then
|
|
onPurchaseFailed(PURCHASE_FAILED.UNDER_13)
|
|
return false
|
|
end
|
|
|
|
if isLimitedUnique() then
|
|
onPurchaseFailed(PURCHASE_FAILED.LIMITED)
|
|
return false
|
|
end
|
|
|
|
if (hasFunds or isFree or invalidBCLevel) then
|
|
if not setPurchaseDataInGui(isFree, invalidBCLevel) then
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--[[ Purchase Functions ]]--
|
|
local function getToolAsset(assetId)
|
|
local tool = InsertService:LoadAsset(assetId)
|
|
if not tool then return nil end
|
|
--
|
|
if tool:IsA("Tool") then
|
|
return tool
|
|
end
|
|
|
|
local children = tool:GetChildren()
|
|
for i = 1, #children do
|
|
if children[i]:IsA("Tool") then
|
|
return children[i]
|
|
end
|
|
end
|
|
end
|
|
|
|
local function onPurchaseSuccess()
|
|
IsCheckingPlayerFunds = false
|
|
local descriptionText = PURCHASE_MSG.SUCCEEDED
|
|
|
|
descriptionText = string.gsub(descriptionText, "itemName", string.sub(PurchaseData.ProductInfo["Name"], 1, 20))
|
|
ItemDescriptionText.Text = descriptionText
|
|
|
|
local playerBalance = getPlayerBalance()
|
|
local currencyType = PurchaseData.CurrencyType == Enum.CurrencyType.Tix and "tickets" or "robux"
|
|
local newBalance = playerBalance[currencyType]
|
|
|
|
if currencyType == "robux" then
|
|
PostBalanceText.Text = PURCHASE_MSG.BALANCE_NOW..getCurrencyString(PurchaseData.CurrencyType)..formatNumber(newBalance).."."
|
|
else
|
|
PostBalanceText.Text = PURCHASE_MSG.BALANCE_NOW..formatNumber(newBalance).." "..getCurrencyString(PurchaseData.CurrencyType).."."
|
|
end
|
|
|
|
if isFreeItem() then PostBalanceText.Visible = false end
|
|
|
|
purchaseState = PURCHASE_STATE.SUCCEEDED
|
|
|
|
setButtonsVisible(OkPurchasedButton)
|
|
stopPurchaseAnimation()
|
|
end
|
|
|
|
local function onAcceptPurchase()
|
|
if IsCurrentlyPurchasing then return end
|
|
|
|
if purchaseState ~= PURCHASE_STATE.BUYITEM then
|
|
return
|
|
end
|
|
|
|
--
|
|
disableControllerInput()
|
|
IsCurrentlyPurchasing = true
|
|
startPurchaseAnimation()
|
|
local startTime = tick()
|
|
local apiPath = nil
|
|
local params = nil
|
|
local currencyTypeInt = nil
|
|
if PurchaseData.CurrencyType == Enum.CurrencyType.Robux or PurchaseData.CurrencyType == Enum.CurrencyType.Default then
|
|
currencyTypeInt = 1
|
|
elseif PurchaseData.CurrencyType == Enum.CurrencyType.Tix then
|
|
currencyTypeInt = 2
|
|
end
|
|
|
|
local productId = PurchaseData.ProductInfo["ProductId"]
|
|
if IsPurchasingConsumable then
|
|
apiPath = "marketplace/submitpurchase"
|
|
params = "productId="..tostring(productId).."¤cyTypeId="..tostring(currencyTypeInt)..
|
|
"&expectedUnitPrice="..tostring(PurchaseData.CurrencyAmount).."&placeId="..tostring(game.PlaceId)
|
|
params = params.."&requestId="..HttpService:UrlEncode(HttpService:GenerateGUID(false))
|
|
else
|
|
apiPath = "marketplace/purchase"
|
|
params = "productId="..tostring(productId).."¤cyTypeId="..tostring(currencyTypeInt)..
|
|
"&purchasePrice="..tostring(PurchaseData.CurrencyAmount or 0).."&locationType=Game&locationId="..tostring(game.PlaceId)
|
|
end
|
|
|
|
local success, result = pcall(function()
|
|
return HttpRbxApiService:PostAsync(apiPath, params, true, Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationUrlEncoded)
|
|
end)
|
|
|
|
-- retry
|
|
if IsPurchasingConsumable then
|
|
local retries = 3
|
|
local wasSuccess = success and result and result ~= ''
|
|
while retries > 0 and not wasSuccess do
|
|
wait(1)
|
|
retries = retries - 1
|
|
success, result = pcall(function()
|
|
return HttpRbxApiService:PostAsync(apiPath, params, true, Enum.ThrottlingPriority.Default, Enum.HttpContentType.ApplicationUrlEncoded)
|
|
end)
|
|
wasSuccess = success and result and result ~= ''
|
|
end
|
|
--
|
|
game:ReportInGoogleAnalytics("Developer Product", "Purchase",
|
|
wasSuccess and ("success. Retries = "..(3 - retries)) or ("failure: " .. tostring(result)), 1)
|
|
end
|
|
|
|
if tick() - startTime < 1 then wait(1) end -- artifical delay to show spinner for at least 1 second
|
|
|
|
enableControllerInput()
|
|
|
|
if not success then
|
|
print("PurchasePromptScript: onAcceptPurchase() failed because", result)
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return
|
|
end
|
|
|
|
result = HttpService:JSONDecode(result)
|
|
if result then
|
|
if result["success"] == false then
|
|
if result["status"] ~= "AlreadyOwned" then
|
|
print("PurchasePromptScript: onAcceptPurchase() response failed because", tostring(result["status"]))
|
|
if result["status"] == "EconomyDisabled" then
|
|
onPurchaseFailed(PURCHASE_FAILED.IN_GAME_PURCHASE_DISABLED)
|
|
else
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
else
|
|
print("PurchasePromptScript: onAcceptPurchase() failed to parse JSON of", productId)
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return
|
|
end
|
|
|
|
if PurchaseData.EquipOnPurchase and PurchaseData.AssetId and tonumber(PurchaseData.ProductInfo["AssetTypeId"]) == 19 then
|
|
local tool = getToolAsset(tonumber(PurchaseData.AssetId))
|
|
if tool then
|
|
tool.Parent = Players.LocalPlayer.Backpack
|
|
end
|
|
end
|
|
|
|
if IsPurchasingConsumable then
|
|
if not result["receipt"] then
|
|
print("PurchasePromptScript: onAcceptPurchase() failed because no dev product receipt was returned for", tostring(productId))
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return
|
|
end
|
|
MarketplaceService:SignalClientPurchaseSuccess(tostring(result["receipt"]), Players.LocalPlayer.userId, productId)
|
|
else
|
|
onPurchaseSuccess()
|
|
end
|
|
end
|
|
|
|
-- main entry point
|
|
local function onPurchasePrompt(player, assetId, equipIfPurchased, currencyType, productId)
|
|
if player == Players.LocalPlayer and not IsCurrentlyPrompting then
|
|
IsCurrentlyPrompting = true
|
|
setInitialPurchaseData(assetId, productId, currencyType, equipIfPurchased)
|
|
if canPurchase() then
|
|
showPurchasePrompt()
|
|
end
|
|
end
|
|
end
|
|
|
|
function hasEnoughMoneyForPurchase()
|
|
local playerBalance = getPlayerBalance()
|
|
if playerBalance then
|
|
local success, hasFunds = nil
|
|
success, hasFunds = playerHasFundsForPurchase(playerBalance)
|
|
return success and hasFunds
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function retryPurchase(overrideRetries)
|
|
local canMakePurchase = canPurchase(true) and hasEnoughMoneyForPurchase()
|
|
if not canMakePurchase then
|
|
local retries = 40
|
|
if overrideRetries then
|
|
retries = overrideRetries
|
|
end
|
|
while retries > 0 and not canMakePurchase do
|
|
wait(0.5)
|
|
canMakePurchase = canPurchase(true) and hasEnoughMoneyForPurchase()
|
|
retries = retries - 1
|
|
end
|
|
end
|
|
|
|
return canMakePurchase
|
|
end
|
|
|
|
function nativePurchaseFinished(wasPurchased)
|
|
if wasPurchased then
|
|
local isPurchasing = retryPurchase()
|
|
if isPurchasing then
|
|
onAcceptPurchase()
|
|
else
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
end
|
|
else
|
|
onPurchaseFailed(PURCHASE_FAILED.DID_NOT_BUY_ROBUX)
|
|
stopPurchaseAnimation()
|
|
end
|
|
end
|
|
|
|
local function onBuyRobuxPrompt()
|
|
if purchaseState ~= PURCHASE_STATE.BUYROBUX then
|
|
return
|
|
end
|
|
if RunService:IsStudio() then
|
|
return
|
|
end
|
|
|
|
purchaseState = PURCHASE_STATE.BUYINGROBUX
|
|
|
|
startPurchaseAnimation()
|
|
if IsNativePurchasing then
|
|
if platform == Enum.Platform.XBoxOne then
|
|
spawn(function()
|
|
local PlatformService = nil
|
|
pcall(function() PlatformService = Game:GetService('PlatformService') end)
|
|
if PlatformService then
|
|
local platformPurchaseReturnInt = -1
|
|
local purchaseCallSuccess, purchaseErrorMsg = pcall(function()
|
|
platformPurchaseReturnInt = PlatformService:BeginPlatformStorePurchase(ThirdPartyProductName)
|
|
end)
|
|
if purchaseCallSuccess then
|
|
nativePurchaseFinished(platformPurchaseReturnInt == 0)
|
|
else
|
|
nativePurchaseFinished(purchaseCallSuccess)
|
|
end
|
|
end
|
|
end)
|
|
else
|
|
MarketplaceService:PromptNativePurchase(Players.LocalPlayer, ThirdPartyProductName)
|
|
end
|
|
else
|
|
IsCheckingPlayerFunds = true
|
|
GuiService:OpenBrowserWindow(BASE_URL.."Upgrades/Robux.aspx")
|
|
end
|
|
end
|
|
|
|
local function onUpgradeBCPrompt()
|
|
if purchaseState ~= PURCHASE_STATE.BUYBC then
|
|
return
|
|
end
|
|
|
|
IsCheckingPlayerFunds = true
|
|
GuiService:OpenBrowserWindow(BASE_URL.."Upgrades/BuildersClubMemberships.aspx")
|
|
end
|
|
|
|
function enableControllerInput()
|
|
local cas = game:GetService("ContextActionService")
|
|
|
|
--accept the purchase when the user presses the a button
|
|
cas:BindCoreAction(
|
|
CONTROLLER_CONFIRM_ACTION_NAME,
|
|
function(actionName, inputState, inputObject)
|
|
if inputState ~= Enum.UserInputState.Begin then return end
|
|
|
|
if purchaseState == PURCHASE_STATE.SUCCEEDED then
|
|
onPromptEnded()
|
|
elseif purchaseState == PURCHASE_STATE.FAILED then
|
|
onPromptEnded()
|
|
elseif purchaseState == PURCHASE_STATE.BUYITEM then
|
|
onAcceptPurchase()
|
|
elseif purchaseState == PURCHASE_STATE.BUYROBUX then
|
|
onBuyRobuxPrompt()
|
|
elseif purchaseState == PURCHASE_STATE.BUYBC then
|
|
onUpgradeBCPrompt()
|
|
end
|
|
end,
|
|
false,
|
|
Enum.KeyCode.ButtonA
|
|
)
|
|
|
|
--cancel the purchase when the user presses the b button
|
|
cas:BindCoreAction(
|
|
CONTROLLER_CANCEL_ACTION_NAME,
|
|
function(actionName, inputState, inputObject)
|
|
if inputState ~= Enum.UserInputState.Begin then return end
|
|
|
|
if (OkPurchasedButton.Visible or OkButton.Visible or CancelButton.Visible) and (not PurchaseFrame.Visible) then
|
|
onPromptEnded(false)
|
|
end
|
|
end,
|
|
false,
|
|
Enum.KeyCode.ButtonB
|
|
)
|
|
end
|
|
|
|
function disableControllerInput()
|
|
local cas = game:GetService("ContextActionService")
|
|
cas:UnbindCoreAction(CONTROLLER_CONFIRM_ACTION_NAME)
|
|
cas:UnbindCoreAction(CONTROLLER_CANCEL_ACTION_NAME)
|
|
end
|
|
|
|
function showGamepadButtons()
|
|
for _, button in pairs(GAMEPAD_BUTTONS) do
|
|
button.Visible = true
|
|
end
|
|
end
|
|
|
|
function hideGamepadButtons()
|
|
for _, button in pairs(GAMEPAD_BUTTONS) do
|
|
button.Visible = false
|
|
end
|
|
end
|
|
|
|
function valueInTable(val, tab)
|
|
for _, v in pairs(tab) do
|
|
if v == val then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function onInputChanged(inputObject)
|
|
local input = inputObject.UserInputType
|
|
local inputs = Enum.UserInputType
|
|
if valueInTable(input, {inputs.Gamepad1, inputs.Gamepad2, inputs.Gamepad3, inputs.Gamepad4}) then
|
|
if inputObject.KeyCode == Enum.KeyCode.Thumbstick1 or inputObject.KeyCode == Enum.KeyCode.Thumbstick2 then
|
|
if math.abs(inputObject.Position.X) > 0.1 or math.abs(inputObject.Position.Z) > 0.1 or math.abs(inputObject.Position.Y) > 0.1 then
|
|
showGamepadButtons()
|
|
end
|
|
else
|
|
showGamepadButtons()
|
|
end
|
|
else
|
|
hideGamepadButtons()
|
|
end
|
|
end
|
|
UserInputService.InputChanged:connect(onInputChanged)
|
|
UserInputService.InputBegan:connect(onInputChanged)
|
|
hideGamepadButtons()
|
|
|
|
--[[ Event Connections ]]--
|
|
CancelButton.MouseButton1Click:connect(function()
|
|
if IsCurrentlyPurchasing then return end
|
|
onPromptEnded(false)
|
|
end)
|
|
BuyButton.MouseButton1Click:connect(onAcceptPurchase)
|
|
FreeButton.MouseButton1Click:connect(onAcceptPurchase)
|
|
OkButton.MouseButton1Click:connect(function()
|
|
if purchaseState == PURCHASE_STATE.FAILED then
|
|
onPromptEnded(false)
|
|
end
|
|
end)
|
|
OkPurchasedButton.MouseButton1Click:connect(function()
|
|
if purchaseState == PURCHASE_STATE.SUCCEEDED then
|
|
onPromptEnded(true)
|
|
end
|
|
end)
|
|
BuyRobuxButton.MouseButton1Click:connect(onBuyRobuxPrompt)
|
|
BuyBCButton.MouseButton1Click:connect(onUpgradeBCPrompt)
|
|
|
|
MarketplaceService.PromptProductPurchaseRequested:connect(function(player, productId, equipIfPurchased, currencyType)
|
|
onPurchasePrompt(player, nil, equipIfPurchased, currencyType, productId)
|
|
end)
|
|
MarketplaceService.PromptPurchaseRequested:connect(function(player, assetId, equipIfPurchased, currencyType)
|
|
onPurchasePrompt(player, assetId, equipIfPurchased, currencyType, nil)
|
|
end)
|
|
MarketplaceService.ServerPurchaseVerification:connect(function(serverResponseTable)
|
|
if not serverResponseTable then
|
|
onPurchaseFailed(PURCHASE_FAILED.DEFAULT_ERROR)
|
|
return
|
|
end
|
|
|
|
if serverResponseTable["playerId"] and tonumber(serverResponseTable["playerId"]) == Players.LocalPlayer.userId then
|
|
onPurchaseSuccess()
|
|
end
|
|
end)
|
|
|
|
|
|
GuiService.BrowserWindowClosed:connect(function()
|
|
if IsCheckingPlayerFunds then
|
|
retryPurchase(4)
|
|
end
|
|
|
|
onPurchaseFailed(PURCHASE_FAILED.DID_NOT_BUY_ROBUX)
|
|
stopPurchaseAnimation()
|
|
end)
|
|
|
|
if IsNativePurchasing then
|
|
MarketplaceService.NativePurchaseFinished:connect(function(player, productId, wasPurchased)
|
|
nativePurchaseFinished(wasPurchased)
|
|
end)
|
|
end
|