Merge branch 'agent/96e5f11d7896-godot-selling-payout-ui' into main
checkpoint / checkpoint (push) Successful in 5m45s
checkpoint / checkpoint (push) Successful in 5m45s
This commit is contained in:
@@ -0,0 +1,198 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/selling_payout_ui.gd" id="1_spu"]
|
||||||
|
|
||||||
|
[node name="SellingPayoutUI" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_spu")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32.0
|
||||||
|
offset_top = 32.0
|
||||||
|
offset_right = -32.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="HeaderHBox" type="HBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="VBox/HeaderHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "< Back"
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="VBox/HeaderHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
text = "Creator Studio — Selling & Payouts"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="RefreshButton" type="Button" parent="VBox/HeaderHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Refresh"
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="TabContainer" type="TabContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Selling" type="MarginContainer" parent="VBox/TabContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 12
|
||||||
|
theme_override_constants/margin_top = 12
|
||||||
|
theme_override_constants/margin_right = 12
|
||||||
|
theme_override_constants/margin_bottom = 12
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="VBox/TabContainer/Selling"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="CreateListingBox" type="HBoxContainer" parent="VBox/TabContainer/Selling/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="NameInput" type="LineEdit" parent="VBox/TabContainer/Selling/VBox/CreateListingBox"]
|
||||||
|
custom_minimum_size = Vector2(160, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Item Name..."
|
||||||
|
|
||||||
|
[node name="TypeOption" type="OptionButton" parent="VBox/TabContainer/Selling/VBox/CreateListingBox"]
|
||||||
|
custom_minimum_size = Vector2(130, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="DescInput" type="LineEdit" parent="VBox/TabContainer/Selling/VBox/CreateListingBox"]
|
||||||
|
custom_minimum_size = Vector2(180, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Description..."
|
||||||
|
|
||||||
|
[node name="PriceInput" type="LineEdit" parent="VBox/TabContainer/Selling/VBox/CreateListingBox"]
|
||||||
|
custom_minimum_size = Vector2(90, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Price (CR)"
|
||||||
|
|
||||||
|
[node name="CreateBtn" type="Button" parent="VBox/TabContainer/Selling/VBox/CreateListingBox"]
|
||||||
|
custom_minimum_size = Vector2(110, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "List for Sale"
|
||||||
|
|
||||||
|
[node name="ListingsTitle" type="Label" parent="VBox/TabContainer/Selling/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
text = "Active Listings"
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="VBox/TabContainer/Selling/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="ListingsList" type="VBoxContainer" parent="VBox/TabContainer/Selling/VBox/Scroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
|
[node name="Payouts" type="MarginContainer" parent="VBox/TabContainer"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 12
|
||||||
|
theme_override_constants/margin_top = 12
|
||||||
|
theme_override_constants/margin_right = 12
|
||||||
|
theme_override_constants/margin_bottom = 12
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="VBox/TabContainer/Payouts"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="BalanceCard" type="PanelContainer" parent="VBox/TabContainer/Payouts/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="VBox/TabContainer/Payouts/VBox/BalanceCard"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 16
|
||||||
|
theme_override_constants/margin_top = 12
|
||||||
|
theme_override_constants/margin_right = 16
|
||||||
|
theme_override_constants/margin_bottom = 12
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 24
|
||||||
|
|
||||||
|
[node name="AvailTitle" type="Label" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Available to Payout:"
|
||||||
|
|
||||||
|
[node name="AvailableLbl" type="Label" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.2, 0.9, 0.2, 1)
|
||||||
|
text = "0 CR"
|
||||||
|
|
||||||
|
[node name="PendTitle" type="Label" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "In Escrow:"
|
||||||
|
|
||||||
|
[node name="PendingLbl" type="Label" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(1, 0.8, 0.2, 1)
|
||||||
|
text = "0 CR"
|
||||||
|
|
||||||
|
[node name="LifeTitle" type="Label" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Lifetime Earned:"
|
||||||
|
|
||||||
|
[node name="LifetimeLbl" type="Label" parent="VBox/TabContainer/Payouts/VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.4, 0.7, 1, 1)
|
||||||
|
text = "0 CR"
|
||||||
|
|
||||||
|
[node name="RequestBox" type="HBoxContainer" parent="VBox/TabContainer/Payouts/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="AmountInput" type="LineEdit" parent="VBox/TabContainer/Payouts/VBox/RequestBox"]
|
||||||
|
custom_minimum_size = Vector2(130, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Amount (min 100)"
|
||||||
|
|
||||||
|
[node name="DestInput" type="LineEdit" parent="VBox/TabContainer/Payouts/VBox/RequestBox"]
|
||||||
|
custom_minimum_size = Vector2(220, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Payout Destination (e.g. PayPal / IBAN)"
|
||||||
|
|
||||||
|
[node name="SubmitBtn" type="Button" parent="VBox/TabContainer/Payouts/VBox/RequestBox"]
|
||||||
|
custom_minimum_size = Vector2(140, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Request Payout"
|
||||||
|
|
||||||
|
[node name="PayoutsTitle" type="Label" parent="VBox/TabContainer/Payouts/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
text = "Payout Requests History"
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="VBox/TabContainer/Payouts/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="PayoutsList" type="VBoxContainer" parent="VBox/TabContainer/Payouts/VBox/Scroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 6
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
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 Credits 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 Credits!" % [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 CR" % 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 CR" % avail
|
||||||
|
if pending_lbl: pending_lbl.text = "%d CR" % pend
|
||||||
|
if lifetime_lbl: lifetime_lbl.text = "%d CR" % 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 Credits", 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 CR..." % amount)
|
||||||
|
var res: Dictionary = await EconomyApiClient.request_payout(api, amount, dest)
|
||||||
|
if res.ok:
|
||||||
|
show_status("Payout request for %d CR 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 CR" % 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)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless smoke test for SellingPayoutUI scene and script.
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
await process_frame
|
||||||
|
print("[SMOKE] Starting SellingPayoutUI smoke test...")
|
||||||
|
|
||||||
|
var scene = load("res://scenes/selling_payout_ui.tscn")
|
||||||
|
if not scene:
|
||||||
|
printerr("[FAIL] Could not load scenes/selling_payout_ui.tscn")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var ui: Control = scene.instantiate()
|
||||||
|
root.add_child(ui)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# Verify onready nodes
|
||||||
|
assert(ui.back_button != null, "Missing back_button")
|
||||||
|
assert(ui.refresh_button != null, "Missing refresh_button")
|
||||||
|
assert(ui.status_label != null, "Missing status_label")
|
||||||
|
assert(ui.tab_container != null, "Missing tab_container")
|
||||||
|
|
||||||
|
# Selling nodes
|
||||||
|
assert(ui.item_name_input != null, "Missing item_name_input")
|
||||||
|
assert(ui.item_type_option != null, "Missing item_type_option")
|
||||||
|
assert(ui.item_desc_input != null, "Missing item_desc_input")
|
||||||
|
assert(ui.price_input != null, "Missing price_input")
|
||||||
|
assert(ui.create_listing_btn != null, "Missing create_listing_btn")
|
||||||
|
assert(ui.my_listings_container != null, "Missing my_listings_container")
|
||||||
|
|
||||||
|
# Payout nodes
|
||||||
|
assert(ui.available_lbl != null, "Missing available_lbl")
|
||||||
|
assert(ui.pending_lbl != null, "Missing pending_lbl")
|
||||||
|
assert(ui.lifetime_lbl != null, "Missing lifetime_lbl")
|
||||||
|
assert(ui.payout_amount_input != null, "Missing payout_amount_input")
|
||||||
|
assert(ui.payout_dest_input != null, "Missing payout_dest_input")
|
||||||
|
assert(ui.request_payout_btn != null, "Missing request_payout_btn")
|
||||||
|
assert(ui.payouts_container != null, "Missing payouts_container")
|
||||||
|
|
||||||
|
# Test rendering sample listings
|
||||||
|
var sample_listings = [
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"price": 300,
|
||||||
|
"status": "active",
|
||||||
|
"catalog_item": {
|
||||||
|
"name": "Knight Helmet",
|
||||||
|
"item_type": "avatar_item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
ui.render_my_listings(sample_listings)
|
||||||
|
assert(ui.my_listings_container.get_child_count() == 1, "Should render 1 listing row")
|
||||||
|
|
||||||
|
# Test rendering sample payouts
|
||||||
|
var sample_payouts = [
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"amount": 250,
|
||||||
|
"status": "paid",
|
||||||
|
"created_at": "2026-09-15T15:00:00Z",
|
||||||
|
"provider_ref": "stripe_123"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"amount": 100,
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": "2026-09-15T16:00:00Z",
|
||||||
|
"external_ref": "paypal@creator.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
ui.render_payouts(sample_payouts)
|
||||||
|
assert(ui.payouts_container.get_child_count() == 2, "Should render 2 payout rows")
|
||||||
|
|
||||||
|
# Verify signals
|
||||||
|
assert(ui.has_signal("back_requested"), "Missing back_requested signal")
|
||||||
|
|
||||||
|
ui.queue_free()
|
||||||
|
print("[SMOKE] SellingPayoutUI smoke test PASSED")
|
||||||
|
quit(0)
|
||||||
Reference in New Issue
Block a user