extends Control class_name SellingPayoutUI ## Selling & Creator Payout UI panel. ## ## Displays: ## - Tab 1 (Selling): Create new catalog item & marketplace listing, manage my active listings (update/delist) ## - Tab 2 (Payouts): Creator balance breakdown (available/pending/lifetime), submit payout request, view payout history ## ## docs/planning/economy.md US-6 (task 96e5f11d7896). signal back_requested @onready var back_button: Button = $VBox/HeaderHBox/BackButton @onready var refresh_button: Button = $VBox/HeaderHBox/RefreshButton @onready var status_label: Label = $VBox/StatusLabel @onready var tab_container: TabContainer = $VBox/TabContainer # Tab 1: Selling @onready var item_name_input: LineEdit = $VBox/TabContainer/Selling/VBox/CreateListingBox/NameInput @onready var item_type_option: OptionButton = $VBox/TabContainer/Selling/VBox/CreateListingBox/TypeOption @onready var item_desc_input: LineEdit = $VBox/TabContainer/Selling/VBox/CreateListingBox/DescInput @onready var price_input: LineEdit = $VBox/TabContainer/Selling/VBox/CreateListingBox/PriceInput @onready var create_listing_btn: Button = $VBox/TabContainer/Selling/VBox/CreateListingBox/CreateBtn @onready var my_listings_container: VBoxContainer = $VBox/TabContainer/Selling/VBox/Scroll/ListingsList # Tab 2: Payouts @onready var available_lbl: Label = $VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox/AvailableLbl @onready var pending_lbl: Label = $VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox/PendingLbl @onready var lifetime_lbl: Label = $VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox/LifetimeLbl @onready var payout_amount_input: LineEdit = $VBox/TabContainer/Payouts/VBox/RequestBox/AmountInput @onready var payout_dest_input: LineEdit = $VBox/TabContainer/Payouts/VBox/RequestBox/DestInput @onready var request_payout_btn: Button = $VBox/TabContainer/Payouts/VBox/RequestBox/SubmitBtn @onready var payouts_container: VBoxContainer = $VBox/TabContainer/Payouts/VBox/Scroll/PayoutsList var _api_client: Node = null var AudioManager: Node: get: return get_node_or_null("/root/AudioManager") func _ready() -> void: _api_client = _get_api() _setup_type_options() _wire_signals() if _api_client != null and _api_client.has_token(): refresh_all() func _get_api() -> Node: if has_node("/root/ApiClient"): return get_node("/root/ApiClient") return null func _setup_type_options() -> void: if not item_type_option: return item_type_option.clear() item_type_option.add_item("Avatar Item", 0) item_type_option.add_item("Asset", 1) item_type_option.add_item("Experience Pass", 2) func _wire_signals() -> void: if back_button and not back_button.pressed.is_connected(_on_back_pressed): back_button.pressed.connect(_on_back_pressed) if refresh_button and not refresh_button.pressed.is_connected(refresh_all): refresh_button.pressed.connect(refresh_all) if create_listing_btn and not create_listing_btn.pressed.is_connected(_on_create_listing_pressed): create_listing_btn.pressed.connect(_on_create_listing_pressed) if request_payout_btn and not request_payout_btn.pressed.is_connected(_on_request_payout_pressed): request_payout_btn.pressed.connect(_on_request_payout_pressed) func _on_back_pressed() -> void: back_requested.emit() if get_tree() and get_tree().current_scene == self: get_tree().change_scene_to_file("res://scenes/main_menu.tscn") func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): get_viewport().set_input_as_handled() _on_back_pressed() func show_status(text: String, is_error: bool = false) -> void: if not status_label: return status_label.text = text if is_error: status_label.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3)) else: status_label.add_theme_color_override("font_color", Color(0.3, 0.9, 0.3)) func refresh_all() -> void: refresh_my_listings() refresh_creator_balance() refresh_payouts() # ── Selling / Listings ── func refresh_my_listings() -> void: var api = _get_api() if not api or not api.has_token(): return # Fetch my items var items_res: Dictionary = await EconomyApiClient.get_catalog_items(api, true) # Fetch active listings var list_res: Dictionary = await EconomyApiClient.search_listings(api, "", "", -1, -1, 100, 0) if list_res.ok and list_res.data is Dictionary: var all_listings: Array = list_res.data.get("listings", []) # Filter for seller_id or user's items render_my_listings(all_listings) func _on_create_listing_pressed() -> void: var api = _get_api() if not api or not api.has_token(): show_status("Please login to create listings", true) return var item_name: String = item_name_input.text.strip_edges() if item_name_input else "" if item_name == "": show_status("Item name is required", true) return var itype: String = "avatar_item" if item_type_option: match item_type_option.selected: 0: itype = "avatar_item" 1: itype = "asset" 2: itype = "experience_pass" var desc: String = item_desc_input.text.strip_edges() if item_desc_input else "" var price_text: String = price_input.text.strip_edges() if price_input else "" if not price_text.is_valid_int() or price_text.to_int() <= 0: show_status("Valid positive price in Buffalo Chips is required", true) return var price: int = price_text.to_int() show_status("Creating item and listing...") # 1. Create Catalog Item var item_res: Dictionary = await EconomyApiClient.create_catalog_item(api, item_name, itype, desc) if not item_res.ok or not item_res.data is Dictionary: show_status("Failed to create catalog item: " + item_res.detail, true) return var item_id: int = int(item_res.data.get("id", 0)) # 2. Create Listing var list_res: Dictionary = await EconomyApiClient.create_listing(api, item_id, price) if list_res.ok: show_status("Successfully listed '%s' for %d Buffalo Chips!" % [item_name, price]) if item_name_input: item_name_input.text = "" if item_desc_input: item_desc_input.text = "" if price_input: price_input.text = "" refresh_my_listings() else: show_status("Failed to create listing: " + list_res.detail, true) func render_my_listings(listings: Array) -> void: if not my_listings_container: return for c in my_listings_container.get_children(): my_listings_container.remove_child(c) c.queue_free() if listings.is_empty(): var empty_lbl := Label.new() empty_lbl.text = "No listings found." empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) my_listings_container.add_child(empty_lbl) return for l in listings: if not l is Dictionary: continue var row := PanelContainer.new() var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) row.add_child(hbox) var cat_item: Dictionary = l.get("catalog_item", {}) var name_lbl := Label.new() name_lbl.text = str(cat_item.get("name", "Item")) name_lbl.custom_minimum_size = Vector2(160, 0) name_lbl.add_theme_font_size_override("font_size", 14) hbox.add_child(name_lbl) var type_lbl := Label.new() type_lbl.text = str(cat_item.get("item_type", "")).replace("_", " ").capitalize() type_lbl.custom_minimum_size = Vector2(120, 0) type_lbl.add_theme_color_override("font_color", Color(0.5, 0.7, 1.0)) hbox.add_child(type_lbl) var price_lbl := Label.new() price_lbl.text = "%d Buffalo Chips" % int(l.get("price", 0)) price_lbl.custom_minimum_size = Vector2(80, 0) price_lbl.add_theme_color_override("font_color", Color(1.0, 0.8, 0.2)) hbox.add_child(price_lbl) var status_lbl_item := Label.new() var l_status: String = str(l.get("status", "")) status_lbl_item.text = l_status.capitalize() status_lbl_item.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(status_lbl_item) var listing_id: int = int(l.get("id", 0)) if l_status == "active": var delist_btn := Button.new() delist_btn.text = "Delist" delist_btn.pressed.connect(_on_delist_pressed.bind(listing_id)) hbox.add_child(delist_btn) my_listings_container.add_child(row) func _on_delist_pressed(listing_id: int) -> void: var api = _get_api() if not api or not api.has_token(): return show_status("Delisting listing #%d..." % listing_id) var res: Dictionary = await EconomyApiClient.delist_listing(api, listing_id) if res.ok: show_status("Listing #%d delisted" % listing_id) refresh_my_listings() else: show_status("Failed to delist: " + res.detail, true) # ── Creator Balances & Payouts ── func refresh_creator_balance() -> void: var api = _get_api() if not api or not api.has_token(): return var res: Dictionary = await EconomyApiClient.get_creator_balance(api) if res.ok and res.data is Dictionary: var avail: int = int(res.data.get("available", 0)) var pend: int = int(res.data.get("pending", 0)) var life: int = int(res.data.get("lifetime_earned", 0)) if available_lbl: available_lbl.text = "%d Buffalo Chips" % avail if pending_lbl: pending_lbl.text = "%d Buffalo Chips" % pend if lifetime_lbl: lifetime_lbl.text = "%d Buffalo Chips" % life func refresh_payouts() -> void: var api = _get_api() if not api or not api.has_token(): return var res: Dictionary = await EconomyApiClient.get_payouts(api) if res.ok and res.data is Array: render_payouts(res.data) func _on_request_payout_pressed() -> void: var api = _get_api() if not api or not api.has_token(): show_status("Please login to request payouts", true) return var amt_str: String = payout_amount_input.text.strip_edges() if payout_amount_input else "" if not amt_str.is_valid_int() or amt_str.to_int() < 100: show_status("Minimum payout amount is 100 Buffalo Chips", true) return var amount: int = amt_str.to_int() var dest: String = payout_dest_input.text.strip_edges() if payout_dest_input else "" show_status("Submitting payout request for %d Buffalo Chips..." % amount) var res: Dictionary = await EconomyApiClient.request_payout(api, amount, dest) if res.ok: AudioManager.play_coin() show_status("Payout request for %d Buffalo Chips submitted successfully!" % amount) if payout_amount_input: payout_amount_input.text = "" if payout_dest_input: payout_dest_input.text = "" refresh_creator_balance() refresh_payouts() else: show_status("Payout request failed: " + res.detail, true) func render_payouts(payouts: Array) -> void: if not payouts_container: return for c in payouts_container.get_children(): payouts_container.remove_child(c) c.queue_free() if payouts.is_empty(): var empty_lbl := Label.new() empty_lbl.text = "No payout history." empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) payouts_container.add_child(empty_lbl) return for p in payouts: if not p is Dictionary: continue var row := PanelContainer.new() var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) row.add_child(hbox) var date_str: String = str(p.get("created_at", "")) if date_str.length() > 19: date_str = date_str.substr(0, 19).replace("T", " ") var date_lbl := Label.new() date_lbl.text = date_str date_lbl.custom_minimum_size = Vector2(140, 0) date_lbl.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) hbox.add_child(date_lbl) var amt_lbl := Label.new() amt_lbl.text = "%d Buffalo Chips" % int(p.get("amount", 0)) amt_lbl.custom_minimum_size = Vector2(90, 0) amt_lbl.add_theme_font_size_override("font_size", 14) hbox.add_child(amt_lbl) var status_str: String = str(p.get("status", "")).capitalize() var stat_lbl := Label.new() stat_lbl.text = status_str stat_lbl.custom_minimum_size = Vector2(100, 0) if status_str == "Paid": stat_lbl.add_theme_color_override("font_color", Color(0.2, 0.9, 0.2)) elif status_str == "Rejected": stat_lbl.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3)) else: stat_lbl.add_theme_color_override("font_color", Color(1.0, 0.8, 0.2)) hbox.add_child(stat_lbl) var ref_lbl := Label.new() var ext_ref: String = str(p.get("external_ref", "")) var prov_ref: String = str(p.get("provider_ref", "")) if prov_ref != "": ref_lbl.text = "Ref: %s" % prov_ref elif ext_ref != "": ref_lbl.text = "Dest: %s" % ext_ref else: ref_lbl.text = "-" ref_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL ref_lbl.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5)) hbox.add_child(ref_lbl) payouts_container.add_child(row)