<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4"><Item class="Folder" referent="RBX5"><Properties><string name="Name">CreatorCodeHub</string></Properties><Item class="ModuleScript" referent="RBX0"><Properties><string name="Name">README</string><ProtectedString name="Source"><![CDATA[--[[
# CreatorCodeHub – game module

Creator codes for Roblox games. Players enter a creator's code, and for 7 days that creator earns a share of
everything the player buys in your game. You pay creators once a month from your Roblox group; the portal
does the bookkeeping.

## Test in Studio (5 minutes)

1. In your game console on the portal, click **Download personal module**. The file already contains the
   portal address, your game and a test key.
2. Drag the file into Studio and move the `CreatorCodeHub` folder into **ServerScriptService**.
   HTTP requests must be allowed: Game Settings → Security → *Allow HTTP Requests*.
3. Press **Play**, click **Codes**, enter `TESTCODE`, then buy any dev product or game pass.
   Your console shows the check-in, the redemption and the purchase (switch on *Show sandbox data*).

Purchases are tracked automatically. You don't write any code.

## Go live

1. In the console, create the **live key** and copy it.
2. Creator Dashboard → your experience → **Secrets** → new secret named `CreatorCodeHub`, value = live key,
   domain = the portal domain.
3. Set your creator share and payout group in the console, then switch the game live. Publish.

## Optional

**Reward players** for entering a code (recommended, it gives creators something to talk about):

```lua
Config.CreatorCodeReward = function(player, code)
    player.leaderstats.Coins.Value += 500
    return true, "+500 coins"
end
```

Return `false, "reason"` if the reward can't be given yet; the module retries every 10 seconds. It is given at
most once per 7-day binding.

**Exact receipt data** for dev products: set `Config.DevProducts = "receipt"` and install your ProcessReceipt
through the module:

```lua
local CreatorCodeHub = require(game.ServerScriptService.CreatorCodeHub.Service)
CreatorCodeHub.WrapProcessReceipt(function(receiptInfo)
    -- your existing ProcessReceipt logic
    return Enum.ProductPurchaseDecision.PurchaseGranted
end)
```

**Own UI:** set `Config.ShowDefaultUI = false` and call `CreatorCodeHub.Redeem(player, code)` from your server
code. `CreatorCodeHub.GetActiveCode(player)` returns the active code; the player attributes `CreatorCode` and
`CreatorCodeDays` are set automatically for client UI.

## What the module sends

Creator code, a random player reference (GUID from your DataStore), timestamps, and product id, name and price
of purchases. Never Roblox user ids, names or other player data. The portal cannot link a player across games.

## Fail-safe

Every web call is wrapped in `pcall`. If the portal is unreachable, your game keeps working; events wait in a
queue and, at shutdown, in your DataStore, and are sent later.

## Tests without Studio

`test/run.py --luau <luau-cli>` runs the module against simulated Roblox services (redeem, purchases, portal outage,
shutdown) and, with `--portal <url> --key <sandbox key>`, sends the captured requests to a real portal.

]]
return nil
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX1"><Properties><string name="Name">Config</string><ProtectedString name="Source"><![CDATA[-- CreatorCodeHub / Config (ModuleScript)
-- Optional settings. The module works without touching this file:
--   * Your personal download from the portal already contains everything for Studio tests.
--   * Dev products and game passes are tracked automatically.
--
-- Going live (once): Creator Dashboard -> your experience -> Secrets -> add a secret named "CreatorCodeHub"
-- with your LIVE key from the portal, and turn on Game Settings -> Security -> "Allow HTTP Requests".
-- Full guide: https://creatorcodehub.com/docs
--
-- Privacy: the module never sends Roblox user IDs. Each player gets a random reference (GUID) stored in your
-- own DataStore. The portal cannot link players across games.

local Config={}

-- ===== Connection (filled in automatically in your personal download) =====
Config.GameKey=""
Config.PortalUrl="https://creatorcodehub.com"
-- Name of the secret in the Creator Dashboard that holds your LIVE key.
Config.SecretName="CreatorCodeHub"

-- ===== Purchases =====
-- "auto"    : dev products and game passes are tracked automatically (no code needed). Recommended.
-- "receipt" : dev products only through CreatorCodeHub.WrapProcessReceipt / ReportReceipt (see README),
--             game passes stay automatic. Use this if you want exact receipt data.
Config.DevProducts="auto"

-- ===== Built-in code button =====
-- Show the built-in "Codes" button and window. Set to false if you build your own UI (see README, "Own UI").
Config.ShowDefaultUI=true
-- Text language: "auto" (player's Roblox language, English or German), "en" or "de".
Config.Language="auto"
Config.ButtonText="Codes"
-- "BottomLeft" | "BottomRight" | "TopLeft" | "TopRight"
Config.ButtonPosition="BottomLeft"
-- Second row for free promo codes (only if you create promo codes in the portal).
Config.ShowPromoCodes=false

-- ===== Reward for entering a creator code (optional, recommended) =====
-- Called once per 7-day binding. Return true, "text shown to the player" when given;
-- return false, "reason" if it can't be given right now (the module retries every 10 seconds).
Config.CreatorCodeReward=nil
-- Example:
-- Config.CreatorCodeReward=function(player,code)
-- 	local coins=player:FindFirstChild("leaderstats") and player.leaderstats:FindFirstChild("Coins")
-- 	if not coins then return false,"Loading, try again in a moment." end
-- 	coins.Value+=500
-- 	return true,"+500 coins"
-- end

-- Called after every successful creator code activation (for your own analytics). Optional.
Config.OnCodeActivated=nil

-- Rewards for free promo codes: map each reward type you use to a function returning ok, text.
Config.PromoRewards={}

-- Override single texts, e.g. Config.Texts.Activated = "Thanks for supporting {code}!"
Config.Texts={}

-- Seconds between two attempts of the same player.
Config.Cooldown=4
-- DataStore for player references and bindings. Change only before your first launch.
Config.DataStoreName="CreatorCodeHub_v1"

return Config
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX2"><Properties><string name="Name">Service</string><ProtectedString name="Source"><![CDATA[-- CreatorCodeHub / Service (ModuleScript) - version 2.1.0
-- Creator codes for Roblox games: binds a player to a creator for 7 days and reports that player's purchases,
-- so the creator earns a share. Do not edit this file; everything game-specific lives in Config.
--
-- Data that leaves your game (HTTPS to Config.PortalUrl):
--   * creator code, a random player reference (GUID), timestamps, product id/name/price of purchases.
--   * NEVER the Roblox user id, name or any other player data.
-- The player reference and the 7-day binding are stored in YOUR DataStore (Config.DataStoreName).
--
-- Fail-safe: every web call is wrapped in pcall. If the portal is down, purchases keep working normally;
-- events wait in a queue (and in your DataStore at shutdown) and are sent later.
--
-- Purchases are tracked automatically (Config.DevProducts = "auto"): game passes after Roblox confirms ownership,
-- dev products when the purchase prompt reports a completed purchase. For exact receipt data you can switch to
-- Config.DevProducts = "receipt" and use WrapProcessReceipt / ReportReceipt.
--
-- Public API (require(ServerScriptService.CreatorCodeHub.Service)):
--   Service.WrapProcessReceipt(handler)  -> sets MarketplaceService.ProcessReceipt to your handler and reports
--                                          granted dev product purchases (exact mode)
--   Service.ReportReceipt(receiptInfo)   -> report one granted dev product purchase yourself (own ProcessReceipt)
--   Service.Redeem(player, code)         -> ok:boolean, message:string (creator code; used by the built-in UI)
--   Service.RedeemPromo(player, code)    -> ok:boolean, message:string (free promo code)
--   Service.GetActiveCode(player)        -> string? (active creator code of this player)
--   Service.GetRemainingDays(player)     -> number
--   Service.PromptCodeEntry(player)      -> opens the built-in code window on that player's screen
--   Service.CodeActivated                -> BindableEvent:Event(player, code)
local HttpService=game:GetService("HttpService")
local DataStoreService=game:GetService("DataStoreService")
local MarketplaceService=game:GetService("MarketplaceService")
local Players=game:GetService("Players")
local RunService=game:GetService("RunService")
local ServerStorage=game:GetService("ServerStorage")

local Config=require(script.Parent:WaitForChild("Config"))

local Service={}
Service.Version="2.1.0"

local IS_STUDIO=RunService:IsStudio()
local PORTAL=tostring(Config.PortalUrl or ""):gsub("/+$","")
local COOLDOWN=tonumber(Config.Cooldown) or 4
local BINDING_DAYS=7
local FLUSH_INTERVAL=5
local FLUSH_BATCH=50
local QUEUE_CAP=1000
local RESOLVE_TTL_OK=300
local RESOLVE_TTL_FAIL=60

local function warnf(f,...) warn(("[CreatorCodeHub] "..f):format(...)) end
local function isoNow(t) return os.date("!%Y-%m-%dT%H:%M:%SZ",t or os.time()) end
local function bindingSeconds() return BINDING_DAYS*86400 end

-- ===== Texts =====
local DEFAULT_TEXTS={
	en={
		InvalidCode="Invalid code",
		Throttled="Please wait a moment",
		Loading="Still loading, try again in a moment.",
		Unavailable="Not available right now, try again later.",
		Unreachable="Can't reach the code service, try again later.",
		ActiveSame="Already active, {days} days left.",
		ActiveOther="You can enter a new code in {days} days.",
		Unknown="Unknown creator code",
		NotMember="This code doesn't work in this game",
		Paused="This creator code is paused",
		SelfCode="You can't use your own code",
		Activated="{code} is active for {days} days. Thanks!",
		ActivatedReward="Active! Reward: {reward}",
		RewardPending="Active! Your reward follows shortly.",
		PromoReasons={unknown="Unknown code",inactive="Code no longer active",expired="Code expired",not_started="Code not valid yet",exhausted="Code used up",already_redeemed="Already redeemed",too_many_attempts="Too many attempts, try again tomorrow"},
		PromoFallback="Couldn't redeem",
		RewardFailed="Reward failed",
		Redeemed="Redeemed: {reward}",
		-- Built-in UI
		Title="Codes",
		CreatorTitle="Creator code",
		CreatorText="Support a creator: your purchases count for them for 7 days.",
		CreatorButton="ACTIVATE",
		PromoTitle="Free code",
		PromoText="Redeem codes from videos and Discord.",
		PromoButton="REDEEM",
		Placeholder="ENTER CODE",
		ActiveLabel="Active: {code} ({days} days)",
		Connection="Connection failed, please try again",
		Busy="Previous request still running",
		Error="Something went wrong, try again later",
	},
	de={
		InvalidCode="Ungültiger Code",
		Throttled="Bitte kurz warten",
		Loading="Daten laden noch, gleich nochmal.",
		Unavailable="Gerade nicht verfügbar, später erneut.",
		Unreachable="Code-Dienst nicht erreichbar, später erneut.",
		ActiveSame="Schon aktiv, noch {days} Tage.",
		ActiveOther="Neuer Code erst in {days} Tagen möglich.",
		Unknown="Unbekannter Creator-Code",
		NotMember="Dieser Code gilt in diesem Spiel nicht",
		Paused="Dieser Creator-Code ist pausiert",
		SelfCode="Den eigenen Code kannst du nicht nutzen",
		Activated="{code} aktiv für {days} Tage. Danke!",
		ActivatedReward="Aktiv! Belohnung: {reward}",
		RewardPending="Aktiv! Deine Belohnung folgt gleich.",
		PromoReasons={unknown="Unbekannter Code",inactive="Code nicht mehr aktiv",expired="Code abgelaufen",not_started="Code noch nicht gültig",exhausted="Code schon zu oft eingelöst",already_redeemed="Schon eingelöst",too_many_attempts="Zu viele Versuche, morgen wieder"},
		PromoFallback="Einlösen fehlgeschlagen",
		RewardFailed="Belohnung fehlgeschlagen",
		Redeemed="Eingelöst: {reward}",
		Title="Codes",
		CreatorTitle="Creator-Code",
		CreatorText="Unterstütze einen Creator: Deine Käufe zählen 7 Tage lang für ihn.",
		CreatorButton="AKTIVIEREN",
		PromoTitle="Gratis-Code",
		PromoText="Codes aus Videos und vom Discord hier einlösen.",
		PromoButton="EINLÖSEN",
		Placeholder="CODE EINGEBEN",
		ActiveLabel="Aktiv: {code} (noch {days} Tage)",
		Connection="Verbindung fehlgeschlagen, bitte erneut versuchen",
		Busy="Vorherige Anfrage läuft noch",
		Error="Fehler, bitte später erneut versuchen",
	},
}
-- Language per player: Config.Language "auto" follows the player's Roblox language (German or English)
local function langFor(player)
	local l=Config.Language
	if l=="de" or l=="en" then return l end
	local loc=player and tostring(player.LocaleId or "") or ""
	return loc:sub(1,2)=="de" and "de" or "en"
end
local function textsFor(lang)
	local base=DEFAULT_TEXTS[lang] or DEFAULT_TEXTS.en
	return setmetatable({},{__index=function(_,k)
		local v=type(Config.Texts)=="table" and Config.Texts[k] or nil
		if v~=nil then return v end
		return base[k]
	end})
end
local TEXTS_BY_LANG={en=textsFor("en"),de=textsFor("de")}
local function T(player) return TEXTS_BY_LANG[langFor(player)] end
Service.TextsFor=T
Service.Texts=TEXTS_BY_LANG[(Config.Language=="de" and "de") or "en"]
Service.AllTexts=TEXTS_BY_LANG
local function fill(template,vars)
	return (tostring(template):gsub("{(%w+)}",function(key) local v=vars[key] return v~=nil and tostring(v) or "" end))
end

-- ===== Portal requests =====
local function sandboxKey()
	-- Personal download: StringValue "SandboxKey" inside this package (ServerScriptService is never sent to players)
	local v=script.Parent:FindFirstChild("SandboxKey")
	if not (v and v:IsA("StringValue") and v.Value~="") then v=ServerStorage:FindFirstChild("CreatorCodeHubSandboxKey") end
	if v and v:IsA("StringValue") and v.Value~="" then return v.Value end
	return nil
end
local function authHeaders()
	local headers={["Content-Type"]="application/json",["X-Module-Version"]=Service.Version}
	if type(Config.GameKey)=="string" and Config.GameKey~="" then headers["X-Game-Key"]=Config.GameKey end
	-- In Studio the sandbox key wins, so tests never create real earnings by accident.
	if IS_STUDIO then
		local key=sandboxKey()
		if key then
			headers.Authorization="Bearer "..key
			return headers
		end
	end
	local ok,secret=pcall(function() return HttpService:GetSecret(Config.SecretName) end)
	if ok and secret then
		headers.Authorization=secret:AddPrefix("Bearer ")
		return headers
	end
	return nil
end

-- -> ok:boolean, data:table|string, status:number?
local function request(method,path,body)
	local headers=authHeaders()
	if not headers then return false,"no-key",nil end
	local ok,res=pcall(function()
		return HttpService:RequestAsync({Url=PORTAL..path,Method=method,Headers=headers,Body=body and HttpService:JSONEncode(body) or nil})
	end)
	if not ok then return false,tostring(res),nil end
	local okJson,data=pcall(HttpService.JSONDecode,HttpService,res.Body or "")
	if not res.Success then return false,okJson and data or ("HTTP "..tostring(res.StatusCode)),res.StatusCode end
	return true,okJson and data or {},res.StatusCode
end

-- ===== Program (game settings from the portal) =====
-- Connection status for the built-in UI (shown to developers in Studio only):
-- "connecting" | "ok" | "http_off" | "no_key" | "key_rejected" | "outdated" | "unreachable"
local statusSink=nil
Service.Status="connecting"
local function setStatus(s)
	Service.Status=s
	if statusSink then pcall(statusSink,s) end
end
function Service._setStatusSink(fn) statusSink=fn pcall(fn,Service.Status) end

local program=nil
local function loadProgram()
	if not HttpService.HttpEnabled then
		warnf("HTTP requests are off. Turn on Game Settings -> Security -> 'Allow HTTP Requests', then restart.")
		setStatus("http_off")
		return false
	end
	local ok,data,status=request("GET","/api/v2/program",nil)
	if ok and type(data)=="table" and data.ok then
		program=data
		setStatus("ok")
		if tonumber(data.bindingDays) then BINDING_DAYS=tonumber(data.bindingDays) end
		local mode=data.sandbox and (data.testMode and "test mode" or "sandbox key") or "live"
		print(("[CreatorCodeHub] connected: %s (%s)"):format(tostring(data.game and data.game.name or Config.GameKey),mode))
		return true
	end
	setStatus((data=="no-key" and "no_key") or (status==401 and "key_rejected") or (status==410 and "outdated") or "unreachable")
	if data=="no-key" then
		if IS_STUDIO then
			warnf("no test key found. Download your personal module from your game console on the portal and replace this folder.")
		else
			warnf("no live key: add a secret named '%s' with your live key (Creator Dashboard -> your experience -> Secrets).",Config.SecretName)
		end
	elseif status==401 then
		warnf("key rejected. Download your personal module again (Studio) or check the '%s' secret (live).",Config.SecretName)
	elseif status==410 then
		warnf("this module version is no longer supported, please download the current one from the portal")
	else
		warnf("portal not reachable (%s), retrying",tostring(status or data))
	end
	return false
end

-- ===== Player records: reference + binding (your DataStore) =====
local store=nil
if not IS_STUDIO then
	local okStore,s=pcall(function() return DataStoreService:GetDataStore(Config.DataStoreName) end)
	if okStore then store=s else warnf("DataStore unavailable: %s",tostring(s)) end
end
local function recordKey(uid) return "p_"..tostring(uid) end
local function newRef()
	local ok,guid=pcall(function() return HttpService:GenerateGUID(false) end)
	if ok and type(guid)=="string" then return guid end
	-- Fallback in the same 8-4-4-4-12 hex format the portal expects
	local function hex(n) local t={} for i=1,n do t[i]=string.format("%x",math.random(0,15)) end return table.concat(t) end
	return hex(8).."-"..hex(4).."-"..hex(4).."-"..hex(4).."-"..hex(12)
end

local records={}   -- [UserId] = {ref=string?, code=string?, since=number?, bonus="pending"|"granted"|nil}
local loadState={} -- [UserId] = "loading" | "ready" | "memory" | "failed"
local bonusBusy={}
local lastTry={}

function Service.GetActiveCode(player)
	local r=records[player.UserId]
	if not r or type(r.code)~="string" or type(r.since)~="number" then return nil end
	if os.time()-r.since>bindingSeconds() then return nil end
	return r.code
end
function Service.GetRemainingDays(player)
	local r=records[player.UserId]
	if not r or type(r.since)~="number" then return 0 end
	local rest=bindingSeconds()-(os.time()-r.since)
	return rest>0 and math.ceil(rest/86400) or 0
end
local function publishAttributes(player)
	local code=Service.GetActiveCode(player)
	player:SetAttribute("CreatorCode",code)
	player:SetAttribute("CreatorCodeDays",code and Service.GetRemainingDays(player) or nil)
end

local tryBonus
local function loadRecord(player)
	local uid=player.UserId
	if loadState[uid]=="loading" then return end
	if IS_STUDIO or not store then
		-- Studio never touches real DataStores: test bindings would otherwise block real players for 7 days.
		records[uid]=records[uid] or {ref=newRef()}
		loadState[uid]="memory"
		publishAttributes(player)
		return
	end
	loadState[uid]="loading"
	for attempt=1,4 do
		local ok,data=pcall(function() return store:GetAsync(recordKey(uid)) end)
		if player.Parent==nil then loadState[uid]=nil return end
		if ok then
			records[uid]=type(data)=="table" and data or {}
			loadState[uid]="ready"
			break
		end
		warnf("could not load player data (attempt %d): %s",attempt,tostring(data))
		task.wait(4*attempt)
	end
	if loadState[uid]=="loading" then loadState[uid]="failed" end
	publishAttributes(player)
	if tryBonus then task.spawn(tryBonus,player) end
end

-- Atomic claim: only if there is no valid binding. -> "ok", record | "conflict", record | "error"
local function claimBinding(player,code,withBonus)
	local uid=player.UserId
	local now=os.time()
	if loadState[uid]=="memory" then
		local r=records[uid] or {ref=newRef()}
		r.code=code r.since=now r.bonus=withBonus and "pending" or nil
		records[uid]=r
		return "ok",r
	end
	local result,conflict
	local ok,err=pcall(function()
		store:UpdateAsync(recordKey(uid),function(old)
			old=type(old)=="table" and old or {}
			result=nil conflict=nil
			if type(old.ref)~="string" then old.ref=newRef() end
			if type(old.since)=="number" and type(old.code)=="string" and os.time()-old.since<=bindingSeconds() then
				conflict=old
				return nil
			end
			old.code=code old.since=now old.bonus=withBonus and "pending" or nil
			result=old
			return old
		end)
	end)
	if conflict then return "conflict",conflict end
	if ok and result then return "ok",result end
	warnf("could not save binding: %s",tostring(err))
	return "error",nil
end

-- Store bonus state for the current binding only (never for a newer one).
local function persistBonus(player,r,value)
	if loadState[player.UserId]=="memory" then return true end
	local applied=false
	local ok=pcall(function()
		store:UpdateAsync(recordKey(player.UserId),function(old)
			applied=false
			if type(old)~="table" or old.since~=r.since or old.code~=r.code then return nil end
			old.bonus=value applied=true
			return old
		end)
	end)
	return ok and applied
end

-- ===== Event queue =====
local queue={redemptions={},purchases={}}
local dropped=0
local function enqueue(kind,item)
	local list=queue[kind]
	if #list>=QUEUE_CAP then table.remove(list,1) dropped+=1 warnf("queue full, %d events dropped",dropped) end
	table.insert(list,item)
end
local flushing=false
local function flush()
	if flushing or (#queue.redemptions==0 and #queue.purchases==0) then return end
	flushing=true
	local batch={sentAt=isoNow(),redemptions={},purchases={}}
	for i=1,math.min(FLUSH_BATCH,#queue.redemptions) do batch.redemptions[i]=queue.redemptions[i] end
	for i=1,math.min(FLUSH_BATCH,#queue.purchases) do batch.purchases[i]=queue.purchases[i] end
	local ok,res,status=request("POST","/api/v2/events",batch)
	if ok or status==400 then
		-- 400 = the portal rejected the batch format: retrying would block the queue forever
		if status==400 then warnf("portal rejected a batch: %s",HttpService:JSONEncode(res)) end
		for _=1,#batch.redemptions do table.remove(queue.redemptions,1) end
		for _=1,#batch.purchases do table.remove(queue.purchases,1) end
	end
	flushing=false
end

-- Unsent events at shutdown go to your DataStore and are sent by the next server.
local SPILL_PREFIX="pending_"
local function spillQueue()
	if IS_STUDIO or not store or (#queue.redemptions==0 and #queue.purchases==0) then return end
	local key=SPILL_PREFIX..tostring(os.time()).."_"..((game.JobId~="" and game.JobId or newRef()):gsub("-",""):sub(1,12))
	local ok=pcall(function() store:SetAsync(key,{redemptions=queue.redemptions,purchases=queue.purchases}) end)
	-- Saved: drop from the queue so nothing is sent twice (the portal de-duplicates anyway)
	if ok then queue.redemptions={} queue.purchases={} end
end
local function recoverSpilled()
	if IS_STUDIO or not store then return end
	local ok,pages=pcall(function() return store:ListKeysAsync(SPILL_PREFIX) end)
	if not ok then return end
	local okPage,keys=pcall(function() return pages:GetCurrentPage() end)
	if not okPage then return end
	for _,info in ipairs(keys) do
		local key=info.KeyName
		local data
		-- Take ownership atomically: only one server reads and deletes a spilled queue.
		local okTake=pcall(function() data=store:RemoveAsync(key) end)
		if okTake and type(data)=="table" then
			for _,r in ipairs(data.redemptions or {}) do enqueue("redemptions",r) end
			for _,p in ipairs(data.purchases or {}) do enqueue("purchases",p) end
		end
	end
end

-- ===== Code check against the portal (cached) =====
local resolveCache={}
local function resolve(code)
	local c=resolveCache[code]
	if c and os.clock()-c.at<(c.result.ok and RESOLVE_TTL_OK or RESOLVE_TTL_FAIL) then return c.result end
	local ok,data=request("POST","/api/v2/resolve",{code=code})
	if not ok or type(data)~="table" then return nil end
	resolveCache[code]={at=os.clock(),result=data}
	return data
end

local function normalize(raw)
	if type(raw)~="string" then return nil end
	local code=string.upper(raw):gsub("%s+","")
	if #code<2 or #code>32 or not code:match("^[A-Z0-9_]+$") then return nil end
	return code
end
local function throttled(player,kind)
	local key=player.UserId..kind
	local last=lastTry[key]
	if last and os.clock()-last<COOLDOWN then return true end
	lastTry[key]=os.clock()
	return false
end

-- ===== Reward for entering a creator code =====
local function callReward(player,code)
	if type(Config.CreatorCodeReward)~="function" then return false,nil end
	local ok,granted,text=pcall(Config.CreatorCodeReward,player,code)
	if not ok then warnf("CreatorCodeReward failed: %s",tostring(granted)) return false,nil end
	return granted==true,text
end
-- Give the reward at most once per binding: mark as granted first, then give (a lost reward after a save
-- error is better than a doubled one). If the game says "not now", retry every 10 s while the player is online.
tryBonus=function(player)
	local uid=player.UserId
	local r=records[uid]
	if not r or r.bonus~="pending" or bonusBusy[uid] or player.Parent==nil or not Service.GetActiveCode(player) then return nil end
	bonusBusy[uid]=true
	local result,text=nil,nil
	if persistBonus(player,r,"granted") then
		r.bonus="granted"
		local granted,msg=callReward(player,r.code)
		if granted then
			result,text=true,msg
		else
			r.bonus="pending"
			persistBonus(player,r,"pending")
			result,text=false,msg
		end
	end
	bonusBusy[uid]=nil
	return result,text
end

-- ===== Creator code =====
Service.CodeActivated=Instance.new("BindableEvent")

function Service.Redeem(player,raw)
	local Texts=T(player)
	if throttled(player,"c") then return false,Texts.Throttled end
	local code=normalize(raw)
	if not code then return false,Texts.InvalidCode end
	local uid=player.UserId
	if loadState[uid]==nil or loadState[uid]=="loading" then return false,Texts.Loading end
	if loadState[uid]=="failed" then return false,Texts.Unavailable end
	local active=Service.GetActiveCode(player)
	if active then
		local days=Service.GetRemainingDays(player)
		return false,fill(active==code and Texts.ActiveSame or Texts.ActiveOther,{days=days})
	end
	local res=resolve(code)
	if not res then return false,Texts.Unreachable end
	if res.ok~=true then
		local reason=res.reason
		if reason=="paused" then return false,Texts.Paused end
		if reason=="not_member" or reason=="blocked" then return false,Texts.NotMember end
		return false,Texts.Unknown
	end
	if type(res.creator)=="table" and tonumber(res.creator.userId)==uid then return false,Texts.SelfCode end
	local withBonus=type(Config.CreatorCodeReward)=="function"
	local claim,r=claimBinding(player,code,withBonus)
	if claim=="conflict" then
		records[uid]=r
		publishAttributes(player)
		local days=Service.GetRemainingDays(player)
		return false,fill(r.code==code and Texts.ActiveSame or Texts.ActiveOther,{days=days})
	elseif claim~="ok" then
		return false,Texts.Unavailable
	end
	records[uid]=r
	publishAttributes(player)
	enqueue("redemptions",{id=("rd:%s:%d"):format(r.ref,r.since),ref=r.ref,code=code,at=isoNow(r.since)})
	Service.CodeActivated:Fire(player,code)
	if type(Config.OnCodeActivated)=="function" then
		task.spawn(function()
			local ok,err=pcall(Config.OnCodeActivated,player,code)
			if not ok then warnf("OnCodeActivated failed: %s",tostring(err)) end
		end)
	end
	if withBonus then
		local granted,text=tryBonus(player)
		if granted then return true,fill(Texts.ActivatedReward,{code=code,reward=text or ""}) end
		return true,Texts.RewardPending
	end
	return true,fill(Texts.Activated,{code=code,days=BINDING_DAYS})
end

-- ===== Free promo codes =====
function Service.RedeemPromo(player,raw)
	local Texts=T(player)
	if throttled(player,"p") then return false,Texts.Throttled end
	local code=normalize(raw)
	if not code then return false,Texts.InvalidCode end
	local r=records[player.UserId]
	if not r then return false,Texts.Loading end
	if type(r.ref)~="string" then
		-- A reference is needed for "once per player"; create it now (saved with the next binding or right here)
		r.ref=newRef()
		if loadState[player.UserId]=="ready" then
			pcall(function()
				store:UpdateAsync(recordKey(player.UserId),function(old)
					old=type(old)=="table" and old or {}
					if type(old.ref)=="string" then r.ref=old.ref return nil end
					old.ref=r.ref
					return old
				end)
			end)
		end
	end
	local ok,res=request("POST","/api/v2/promo/redeem",{sentAt=isoNow(),code=code,ref=r.ref})
	if not ok or type(res)~="table" then return false,Texts.Unreachable end
	if res.ok~=true then return false,(type(Texts.PromoReasons)=="table" and Texts.PromoReasons[res.reason]) or Texts.PromoFallback end
	local reward=type(res.reward)=="table" and res.reward or {}
	local fn=type(Config.PromoRewards)=="table" and Config.PromoRewards[reward.type] or nil
	local granted,text=false,nil
	if type(fn)=="function" then
		local okCall,g,t=pcall(fn,player,reward)
		if okCall then granted,text=g==true,t else warnf("PromoRewards.%s failed: %s",tostring(reward.type),tostring(g)) end
	else
		warnf("no PromoRewards handler for type '%s'",tostring(reward.type))
	end
	if not granted then
		-- Give the redemption back, so the player can try again later
		task.spawn(function() request("POST","/api/v2/promo/release",{sentAt=isoNow(),code=code,ref=r.ref}) end)
		return false,text or Texts.RewardFailed
	end
	return true,fill(Texts.Redeemed,{reward=text or "",code=code})
end

-- ===== Purchases =====
-- Report a GRANTED dev product purchase. Safe to call more than once for the same receipt (de-duplicated by PurchaseId).
local receiptMode=Config.DevProducts=="receipt" -- true as soon as the game reports receipts itself
local recentReceipt={} -- [UserId..":"..ProductId] = os.clock() of the last reported receipt
function Service.ReportReceipt(receiptInfo)
	if type(receiptInfo)~="table" then return end
	receiptMode=true
	recentReceipt[tostring(receiptInfo.PlayerId)..":"..tostring(receiptInfo.ProductId)]=os.clock()
	local player=Players:GetPlayerByUserId(receiptInfo.PlayerId)
	if not player then return end
	local code=Service.GetActiveCode(player)
	local r=records[player.UserId]
	local robux=tonumber(receiptInfo.CurrencySpent) or 0
	if not code or not r or type(r.ref)~="string" or robux<=0 then return end
	local item={id="dp:"..tostring(receiptInfo.PurchaseId),ref=r.ref,code=code,type="devproduct",productId=receiptInfo.ProductId,robux=robux,at=isoNow()}
	task.spawn(function()
		pcall(function() item.productName=string.sub(MarketplaceService:GetProductInfo(receiptInfo.ProductId,Enum.InfoType.Product).Name,1,120) end)
		enqueue("purchases",item)
	end)
end

-- Install your ProcessReceipt through this wrapper: granted purchases are reported automatically.
function Service.WrapProcessReceipt(handler)
	assert(type(handler)=="function","WrapProcessReceipt expects your ProcessReceipt function")
	MarketplaceService.ProcessReceipt=function(receiptInfo)
		local decision=handler(receiptInfo)
		if decision==Enum.ProductPurchaseDecision.PurchaseGranted then
			local ok,err=pcall(Service.ReportReceipt,receiptInfo)
			if not ok then warnf("ReportReceipt failed: %s",tostring(err)) end
		end
		return decision
	end
end

-- Game passes: reported automatically after Roblox confirms ownership (protects against fake events).
local function onGamePass(player,passId,wasPurchased)
	if not wasPurchased then return end
	local code=Service.GetActiveCode(player)
	local r=records[player.UserId]
	if not code or not r or type(r.ref)~="string" then return end
	local owns=false
	for attempt=1,3 do
		local ok,result=pcall(function() return MarketplaceService:UserOwnsGamePassAsync(player.UserId,passId) end)
		if ok then owns=result==true break end
		task.wait(2*attempt)
	end
	if not owns then return end
	local price,name=0,nil
	pcall(function()
		local info=MarketplaceService:GetProductInfo(passId,Enum.InfoType.GamePass)
		price=tonumber(info.PriceInRobux) or 0
		name=info.Name and string.sub(info.Name,1,120) or nil
	end)
	if price<=0 then return end
	enqueue("purchases",{id=("gp:%s:%d"):format(r.ref,passId),ref=r.ref,code=code,type="gamepass",productId=passId,productName=name,robux=price,at=isoNow()})
end

-- Dev products without any code (Config.DevProducts = "auto"): the server-side prompt event tells us a purchase was
-- completed. It can't be faked by clients. If the game also reports receipts (ReportReceipt), those win: the auto
-- event waits a moment and is skipped when a receipt for the same player and product arrived.
local function onDevProductPrompt(userId,productId,isPurchased)
	if not isPurchased or receiptMode then return end
	task.wait(8)
	if receiptMode then return end
	local last=recentReceipt[tostring(userId)..":"..tostring(productId)]
	if last and os.clock()-last<60 then return end
	local player=Players:GetPlayerByUserId(userId)
	if not player then return end
	local code=Service.GetActiveCode(player)
	local r=records[player.UserId]
	if not code or not r or type(r.ref)~="string" then return end
	local price,name=0,nil
	pcall(function()
		local info=MarketplaceService:GetProductInfo(productId,Enum.InfoType.Product)
		price=tonumber(info.PriceInRobux) or 0
		name=info.Name and string.sub(info.Name,1,120) or nil
	end)
	if price<=0 then return end
	enqueue("purchases",{id=("pp:%s:%d:%d"):format(r.ref,productId,os.time()),ref=r.ref,code=code,type="devproduct",productId=productId,productName=name,robux=price,at=isoNow()})
end

-- ===== Built-in UI bridge =====
local openEvent=nil
-- Settings the portal reported at server start (nil until connected): game name, sandbox/test mode, share, bonus texts
function Service.GetProgram() return program end
function Service.PromptCodeEntry(player)
	if openEvent then openEvent:FireClient(player) end
end
function Service._setOpenEvent(ev) openEvent=ev end
function Service._isReady(player) return loadState[player.UserId]=="ready" or loadState[player.UserId]=="memory" end

-- ===== Start =====
local started=false
function Service.Start()
	if started then return end
	started=true
	if not HttpService.HttpEnabled then
		warnf("HTTP requests are off. Turn on Game Settings -> Security -> 'Allow HTTP Requests'.")
	end
	task.spawn(function()
		while not loadProgram() do task.wait(60) end
	end)
	task.spawn(recoverSpilled)
	for _,p in ipairs(Players:GetPlayers()) do task.spawn(loadRecord,p) end
	Players.PlayerAdded:Connect(loadRecord)
	Players.PlayerRemoving:Connect(function(p)
		local uid=p.UserId
		records[uid]=nil loadState[uid]=nil bonusBusy[uid]=nil
		lastTry[uid.."c"]=nil lastTry[uid.."p"]=nil
	end)
	MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player,passId,purchased)
		task.spawn(onGamePass,player,passId,purchased)
	end)
	MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId,productId,purchased)
		task.spawn(onDevProductPrompt,userId,productId,purchased)
	end)
	task.spawn(function() while true do task.wait(FLUSH_INTERVAL) flush() end end)
	-- Pending rewards, failed loads, expiring bindings
	task.spawn(function() while true do task.wait(10)
		for _,p in ipairs(Players:GetPlayers()) do
			local r=records[p.UserId]
			if r and r.bonus=="pending" then task.spawn(tryBonus,p) end
			if loadState[p.UserId]=="failed" then task.spawn(loadRecord,p) end
			if p:GetAttribute("CreatorCode")~=Service.GetActiveCode(p) or (p:GetAttribute("CreatorCodeDays") or 0)~=(Service.GetActiveCode(p) and Service.GetRemainingDays(p) or 0) then publishAttributes(p) end
		end
	end end)
	game:BindToClose(function()
		local deadline=os.clock()+20
		while (#queue.redemptions>0 or #queue.purchases>0) and os.clock()<deadline do flush() task.wait(1) end
		spillQueue()
	end)
end

return Service
]]></ProtectedString></Properties></Item><Item class="Script" referent="RBX3"><Properties><string name="Name">Loader</string><bool name="Disabled">false</bool><token name="RunContext">0</token><ProtectedString name="Source"><![CDATA[-- CreatorCodeHub / Loader (Script) - starts the service and connects the built-in UI.
-- Lives inside the CreatorCodeHub folder in ServerScriptService. Do not edit.
--
-- Creates in ReplicatedStorage:
--   CreatorCodeHub (Folder)
--     Redeem (RemoteFunction)  client -> server: (kind "creator"|"promo", code) -> {ok, message}
--     Open   (RemoteEvent)     server -> client: open the code window (Service.PromptCodeEntry)
-- and copies the ClientUI LocalScript to StarterPlayerScripts (unless Config.ShowDefaultUI = false).
local ReplicatedStorage=game:GetService("ReplicatedStorage")
local StarterPlayer=game:GetService("StarterPlayer")
local Players=game:GetService("Players")
local HttpService=game:GetService("HttpService")

local folder=script.Parent
local Config=require(folder:WaitForChild("Config"))
local Service=require(folder:WaitForChild("Service"))

local MAX_CODE_LENGTH=64

local shared=ReplicatedStorage:FindFirstChild("CreatorCodeHub") or Instance.new("Folder")
shared.Name="CreatorCodeHub"
shared.Parent=ReplicatedStorage
local redeem=shared:FindFirstChild("Redeem") or Instance.new("RemoteFunction")
redeem.Name="Redeem"
redeem.Parent=shared
local open=shared:FindFirstChild("Open") or Instance.new("RemoteEvent")
open.Name="Open"
open.Parent=shared
Service._setOpenEvent(open)
-- Connection status for the Studio hint in the built-in UI (players never see it)
Service._setStatusSink(function(s) shared:SetAttribute("Status",s) end)

-- One request per player at a time (two parallel promo redemptions could both succeed otherwise)
local busy={}
redeem.OnServerInvoke=function(player,kind,raw)
	local texts=Service.TextsFor(player)
	if (kind~="creator" and kind~="promo") or type(raw)~="string" or #raw>MAX_CODE_LENGTH then
		return {ok=false,message=texts.InvalidCode}
	end
	if kind=="promo" and Config.ShowPromoCodes~=true then return {ok=false,message=texts.InvalidCode} end
	local key=player.UserId
	if busy[key] then return {ok=false,message=texts.Busy} end
	busy[key]=true
	local ok,a,b=pcall(kind=="creator" and Service.Redeem or Service.RedeemPromo,player,raw)
	busy[key]=nil
	if not ok then
		warn(("[CreatorCodeHub] %s failed: %s"):format(kind,tostring(a)))
		return {ok=false,message=texts.Error}
	end
	return {ok=a==true,message=b}
end
Players.PlayerRemoving:Connect(function(p) busy[p.UserId]=nil end)

-- Built-in UI: texts and settings travel as attributes on the copied LocalScript
local ui=folder:FindFirstChild("ClientUI")
if ui and Config.ShowDefaultUI~=false then
	-- Texts in both languages; the client picks by the player's Roblox language (Config.Language "auto")
	local texts={}
	for lang,set in pairs(Service.AllTexts) do
		texts[lang]={}
		for _,k in ipairs({"Title","CreatorTitle","CreatorText","CreatorButton","PromoTitle","PromoText","PromoButton","Placeholder","ActiveLabel","Connection"}) do
			texts[lang][k]=set[k]
		end
	end
	local copy=ui:Clone()
	copy:SetAttribute("Texts",HttpService:JSONEncode(texts))
	copy:SetAttribute("Language",tostring(Config.Language or "auto"))
	copy:SetAttribute("ShowPromo",Config.ShowPromoCodes==true)
	copy:SetAttribute("ButtonText",tostring(Config.ButtonText or "Codes"))
	copy:SetAttribute("ButtonPosition",tostring(Config.ButtonPosition or "BottomLeft"))
	copy.Enabled=true
	copy.Parent=StarterPlayer:WaitForChild("StarterPlayerScripts")
	-- Players who joined before this line (rare, e.g. Studio play solo) get the UI in their PlayerGui
	for _,p in ipairs(Players:GetPlayers()) do
		local holder=Instance.new("ScreenGui")
		holder.Name="CreatorCodeHubLoader"
		holder.ResetOnSpawn=false
		local c=copy:Clone()
		c.Parent=holder
		holder.Parent=p:WaitForChild("PlayerGui")
	end
end

Service.Start()
]]></ProtectedString></Properties></Item><Item class="LocalScript" referent="RBX4"><Properties><string name="Name">ClientUI</string><bool name="Disabled">true</bool><ProtectedString name="Source"><![CDATA[-- CreatorCodeHub / ClientUI (LocalScript, disabled in the package) - built-in code window.
-- The Loader copies this script to StarterPlayerScripts and passes texts as attributes. Do not edit;
-- turn it off with Config.ShowDefaultUI = false and build your own UI (see README, "Own UI").
local Players=game:GetService("Players")
local ReplicatedStorage=game:GetService("ReplicatedStorage")
local HttpService=game:GetService("HttpService")

-- Only one window per player, even if the script exists twice (StarterPlayerScripts + PlayerGui)
local player=Players.LocalPlayer
local playerGui=player:WaitForChild("PlayerGui")
if playerGui:FindFirstChild("CreatorCodeHubUI") then return end

local shared=ReplicatedStorage:WaitForChild("CreatorCodeHub")
local redeem=shared:WaitForChild("Redeem")
local openEvent=shared:WaitForChild("Open")

local okTexts,ALL=pcall(HttpService.JSONDecode,HttpService,script:GetAttribute("Texts") or "{}")
if not okTexts or type(ALL)~="table" then ALL={} end
local language=script:GetAttribute("Language")
if language~="de" and language~="en" then language=tostring(player.LocaleId or ""):sub(1,2)=="de" and "de" or "en" end
local T=type(ALL[language])=="table" and ALL[language] or (type(ALL.en)=="table" and ALL.en or {})
local function text(k,fallback) return T[k] or fallback end
local showPromo=script:GetAttribute("ShowPromo")==true

local gui=Instance.new("ScreenGui")
gui.Name="CreatorCodeHubUI"
gui.ResetOnSpawn=false
gui.ZIndexBehavior=Enum.ZIndexBehavior.Sibling
gui.DisplayOrder=50
gui.Parent=playerGui

local corners={
	BottomLeft={anchor=Vector2.new(0,1),pos=UDim2.new(0,16,1,-16),panel=UDim2.new(0,16,1,-72)},
	BottomRight={anchor=Vector2.new(1,1),pos=UDim2.new(1,-16,1,-16),panel=UDim2.new(1,-16,1,-72)},
	TopLeft={anchor=Vector2.new(0,0),pos=UDim2.new(0,16,0,64),panel=UDim2.new(0,16,0,120)},
	TopRight={anchor=Vector2.new(1,0),pos=UDim2.new(1,-16,0,64),panel=UDim2.new(1,-16,0,120)},
}
local corner=corners[script:GetAttribute("ButtonPosition") or "BottomLeft"] or corners.BottomLeft

local function round(parent,radius) local c=Instance.new("UICorner") c.CornerRadius=radius c.Parent=parent end
local function stroke(parent) local s=Instance.new("UIStroke") s.Color=Color3.fromRGB(255,255,255) s.Transparency=0.86 s.Parent=parent end

local toggle=Instance.new("TextButton")
toggle.Name="Toggle"
toggle.AnchorPoint=corner.anchor
toggle.Position=corner.pos
toggle.Size=UDim2.fromOffset(96,44)
toggle.BackgroundColor3=Color3.fromRGB(30,30,36)
toggle.TextColor3=Color3.fromRGB(255,255,255)
toggle.Font=Enum.Font.GothamBold
toggle.TextSize=18
toggle.Text=script:GetAttribute("ButtonText") or "Codes"
toggle.Parent=gui
round(toggle,UDim.new(0,10)) stroke(toggle)

local rowHeight=118
local panel=Instance.new("Frame")
panel.Name="Panel"
panel.AnchorPoint=corner.anchor
panel.Position=corner.panel
panel.Size=UDim2.fromOffset(420,40+rowHeight*(showPromo and 2 or 1)+30)
panel.BackgroundColor3=Color3.fromRGB(24,24,29)
panel.BackgroundTransparency=0.05
panel.Visible=false
panel.Parent=gui
round(panel,UDim.new(0,14)) stroke(panel)

-- Scale the window down on small screens
local scale=Instance.new("UIScale")
scale.Parent=panel
local function updateScale()
	local cam=workspace.CurrentCamera
	local vp=cam and cam.ViewportSize or Vector2.new(1280,720)
	scale.Scale=math.clamp(math.min(vp.X/1280,vp.Y/720)*1.15,0.7,1)
end
updateScale()
workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
	updateScale()
	if workspace.CurrentCamera then workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(updateScale) end
end)
if workspace.CurrentCamera then workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"):Connect(updateScale) end

local title=Instance.new("TextLabel")
title.BackgroundTransparency=1
title.Position=UDim2.fromOffset(16,0)
title.Size=UDim2.new(1,-56,0,40)
title.Font=Enum.Font.GothamBold
title.TextSize=20
title.TextColor3=Color3.fromRGB(255,255,255)
title.TextXAlignment=Enum.TextXAlignment.Left
title.Text=text("Title","Codes")
title.Parent=panel
local close=Instance.new("TextButton")
close.AnchorPoint=Vector2.new(1,0)
close.Position=UDim2.new(1,-10,0,6)
close.Size=UDim2.fromOffset(28,28)
close.BackgroundColor3=Color3.fromRGB(40,40,48)
close.TextColor3=Color3.fromRGB(255,255,255)
close.Font=Enum.Font.GothamBold
close.TextSize=16
close.Text="X"
close.Parent=panel
round(close,UDim.new(1,0))

local body=Instance.new("Frame")
body.BackgroundTransparency=1
body.Position=UDim2.fromOffset(16,40)
body.Size=UDim2.new(1,-32,1,-52)
body.Parent=panel
local layout=Instance.new("UIListLayout")
layout.Padding=UDim.new(0,8)
layout.SortOrder=Enum.SortOrder.LayoutOrder
layout.Parent=body

local function label(parent,props)
	local l=Instance.new("TextLabel")
	l.BackgroundTransparency=1
	l.TextXAlignment=Enum.TextXAlignment.Left
	l.TextWrapped=true
	for k,v in pairs(props) do l[k]=v end
	l.Parent=parent
	return l
end

local function buildRow(order,kind,rowTitle,rowText,buttonText)
	local row=Instance.new("Frame")
	row.BackgroundTransparency=1
	row.Size=UDim2.new(1,0,0,rowHeight)
	row.LayoutOrder=order
	row.Parent=body
	label(row,{Size=UDim2.new(1,0,0,20),Font=Enum.Font.GothamBold,TextSize=15,TextColor3=Color3.fromRGB(255,255,255),Text=rowTitle})
	local desc=label(row,{Position=UDim2.fromOffset(0,20),Size=UDim2.new(1,0,0,32),Font=Enum.Font.Gotham,TextSize=12,TextColor3=Color3.fromRGB(170,170,180),TextYAlignment=Enum.TextYAlignment.Top,Text=rowText})
	local input=Instance.new("TextBox")
	input.Position=UDim2.fromOffset(0,56)
	input.Size=UDim2.new(1,-96,0,38)
	input.BackgroundColor3=Color3.fromRGB(36,36,43)
	input.TextColor3=Color3.fromRGB(255,255,255)
	input.PlaceholderText=text("Placeholder","ENTER CODE")
	input.PlaceholderColor3=Color3.fromRGB(120,120,130)
	input.Font=Enum.Font.Gotham
	input.TextSize=14
	input.ClearTextOnFocus=false
	input.Text=""
	input.Parent=row
	round(input,UDim.new(0,8))
	local pad=Instance.new("UIPadding") pad.PaddingLeft=UDim.new(0,12) pad.PaddingRight=UDim.new(0,12) pad.Parent=input
	input:GetPropertyChangedSignal("Text"):Connect(function() if #input.Text>32 then input.Text=string.sub(input.Text,1,32) end end)
	local submit=Instance.new("TextButton")
	submit.AnchorPoint=Vector2.new(1,0)
	submit.Position=UDim2.new(1,0,0,56)
	submit.Size=UDim2.fromOffset(86,38)
	submit.BackgroundColor3=Color3.fromRGB(83,157,229)
	submit.TextColor3=Color3.fromRGB(255,255,255)
	submit.Font=Enum.Font.GothamBold
	submit.TextSize=13
	submit.Text=buttonText
	submit.Parent=row
	round(submit,UDim.new(0,8))
	local status=label(row,{Position=UDim2.fromOffset(0,98),Size=UDim2.new(1,0,0,18),Font=Enum.Font.Gotham,TextSize=12,TextColor3=Color3.fromRGB(150,150,160),Text=""})

	if kind=="creator" then
		local function refresh()
			local code=player:GetAttribute("CreatorCode")
			local days=player:GetAttribute("CreatorCodeDays")
			if code then
				desc.Text=(text("ActiveLabel","Active: {code} ({days} days)"):gsub("{code}",tostring(code)):gsub("{days}",tostring(days or "")))
			else
				desc.Text=rowText
			end
		end
		refresh()
		player:GetAttributeChangedSignal("CreatorCode"):Connect(refresh)
		player:GetAttributeChangedSignal("CreatorCodeDays"):Connect(refresh)
	end

	local busy=false
	local function onSubmit()
		if busy or input.Text=="" then return end
		busy=true
		local previous=submit.Text
		submit.Text="..."
		local ok,result=pcall(function() return redeem:InvokeServer(kind,input.Text) end)
		busy=false
		submit.Text=previous
		if not ok or type(result)~="table" then
			status.Text=text("Connection","Connection failed, please try again")
			status.TextColor3=Color3.fromRGB(230,110,110)
			return
		end
		status.Text=tostring(result.message or "")
		status.TextColor3=result.ok and Color3.fromRGB(120,220,150) or Color3.fromRGB(230,110,110)
		if result.ok then input.Text="" end
	end
	submit.MouseButton1Click:Connect(onSubmit)
	input.FocusLost:Connect(function(enter) if enter then onSubmit() end end)
end

buildRow(1,"creator",text("CreatorTitle","Creator code"),text("CreatorText",""),text("CreatorButton","ACTIVATE"))
if showPromo then buildRow(2,"promo",text("PromoTitle","Free code"),text("PromoText",""),text("PromoButton","REDEEM")) end

-- Studio only: tell the developer what is missing (HTTP off, no key, ...). Players never see this.
if game:GetService("RunService"):IsStudio() then
	local HINTS={
		http_off="CreatorCodeHub: turn on Game Settings > Security > Allow HTTP Requests",
		no_key="CreatorCodeHub: no test key. Download your personal module in the portal console",
		key_rejected="CreatorCodeHub: key rejected. Download your personal module again",
		outdated="CreatorCodeHub: please download the current module from the portal",
		unreachable="CreatorCodeHub: portal not reachable, retrying",
	}
	local hint=Instance.new("TextLabel")
	hint.Name="StudioHint"
	hint.AnchorPoint=corner.anchor
	hint.Position=corner.pos+UDim2.fromOffset(corner.anchor.X==0 and 104 or -104,0)
	hint.Size=UDim2.fromOffset(360,44)
	hint.BackgroundColor3=Color3.fromRGB(120,30,30)
	hint.TextColor3=Color3.fromRGB(255,255,255)
	hint.Font=Enum.Font.GothamMedium
	hint.TextSize=12
	hint.TextWrapped=true
	hint.Visible=false
	hint.Parent=gui
	round(hint,UDim.new(0,8))
	local function refresh()
		local s=shared:GetAttribute("Status")
		hint.Text=HINTS[s] or ""
		hint.Visible=HINTS[s]~=nil
	end
	refresh()
	shared:GetAttributeChangedSignal("Status"):Connect(refresh)
end

local function setOpen(v) panel.Visible=v end
toggle.MouseButton1Click:Connect(function() setOpen(not panel.Visible) end)
close.MouseButton1Click:Connect(function() setOpen(false) end)
openEvent.OnClientEvent:Connect(function() setOpen(true) end)
]]></ProtectedString></Properties></Item></Item></roblox>
