Compare commits

...

4 Commits

Author SHA1 Message Date
daddy ffa9a9e6da feat(godot): marketplace browse, search/filter and buy UI 2026-09-15 20:50:42 +00:00
daddy a4aa50edb9 Merge branch 'agent/34c725431645-godot-wallet-ui' into main
checkpoint / checkpoint (push) Successful in 5m45s
2026-09-15 20:49:52 +00:00
daddy 30ff63f3fd feat(godot): wallet balance, credit pack purchase and ledger history UI 2026-09-15 20:49:43 +00:00
daddy 5c4a45f3c9 Merge branch 'agent/cc09c1428892-godot-economy-api-client' into main
checkpoint / checkpoint (push) Successful in 5m43s
2026-09-15 20:48:20 +00:00
6 changed files with 829 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/marketplace_ui.gd" id="1_mpu"]
[node name="MarketplaceUI" 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_mpu")
[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 = "Marketplace"
horizontal_alignment = 1
[node name="BalanceLabel" type="Label" parent="VBox/HeaderHBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(1, 0.84, 0, 1)
text = "Wallet: 0 CR"
[node name="RefreshButton" type="Button" parent="VBox/HeaderHBox"]
custom_minimum_size = Vector2(100, 36)
layout_mode = 2
text = "Refresh"
[node name="FilterBar" type="HBoxContainer" parent="VBox"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="SearchEdit" type="LineEdit" parent="VBox/FilterBar"]
custom_minimum_size = Vector2(200, 36)
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Search items by name or description..."
[node name="TypeOption" type="OptionButton" parent="VBox/FilterBar"]
custom_minimum_size = Vector2(140, 36)
layout_mode = 2
[node name="MinPriceEdit" type="LineEdit" parent="VBox/FilterBar"]
custom_minimum_size = Vector2(90, 36)
layout_mode = 2
placeholder_text = "Min CR"
[node name="MaxPriceEdit" type="LineEdit" parent="VBox/FilterBar"]
custom_minimum_size = Vector2(90, 36)
layout_mode = 2
placeholder_text = "Max CR"
[node name="SearchBtn" type="Button" parent="VBox/FilterBar"]
custom_minimum_size = Vector2(90, 36)
layout_mode = 2
text = "Search"
[node name="StatusLabel" type="Label" parent="VBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="Scroll" type="ScrollContainer" parent="VBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="ListingsGrid" type="GridContainer" parent="VBox/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 16
theme_override_constants/v_separation = 16
columns = 4
+136
View File
@@ -0,0 +1,136 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/wallet_ui.gd" id="1_wui"]
[node name="WalletUI" 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_wui")
[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 = 16
[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 = "Wallet & Credits"
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="BalanceCard" type="PanelContainer" parent="VBox"]
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="VBox/BalanceCard"]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 16
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 16
[node name="HBox" type="HBoxContainer" parent="VBox/BalanceCard/Margin"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="BalTitle" type="Label" parent="VBox/BalanceCard/Margin/HBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "Available Balance:"
[node name="BalanceLabel" type="Label" parent="VBox/BalanceCard/Margin/HBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "0 CR"
[node name="Spacer" type="Control" parent="VBox/BalanceCard/Margin/HBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PacksHBox" type="HBoxContainer" parent="VBox/BalanceCard/Margin/HBox"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Buy100Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
layout_mode = 2
text = "+100 CR ($0.99)"
[node name="Buy500Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
layout_mode = 2
text = "+500 CR ($4.99)"
[node name="Buy1000Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
layout_mode = 2
text = "+1,000 CR ($9.99)"
[node name="Buy5000Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
layout_mode = 2
text = "+5,000 CR ($44.99)"
[node name="LedgerSection" type="VBoxContainer" parent="VBox"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 8
[node name="LedgerTitle" type="Label" parent="VBox/LedgerSection"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "Ledger History"
[node name="Scroll" type="ScrollContainer" parent="VBox/LedgerSection"]
layout_mode = 2
size_flags_vertical = 3
[node name="TxList" type="VBoxContainer" parent="VBox/LedgerSection/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 6
[node name="PagingHBox" type="HBoxContainer" parent="VBox/LedgerSection"]
layout_mode = 2
alignment = 1
theme_override_constants/separation = 16
[node name="PrevPageBtn" type="Button" parent="VBox/LedgerSection/PagingHBox"]
custom_minimum_size = Vector2(80, 32)
layout_mode = 2
text = "Previous"
[node name="PageLabel" type="Label" parent="VBox/LedgerSection/PagingHBox"]
layout_mode = 2
text = "Page 1 of 1"
[node name="NextPageBtn" type="Button" parent="VBox/LedgerSection/PagingHBox"]
custom_minimum_size = Vector2(80, 32)
layout_mode = 2
text = "Next"
+231
View File
@@ -0,0 +1,231 @@
extends Control
class_name MarketplaceUI
## Marketplace Browse & Buy UI panel.
##
## Displays:
## - Search bar with keyword, item type, and price filters
## - Marketplace active listings cards
## - Item details and direct atomic purchase flow
## - Wallet balance and real-time status feedback
##
## docs/planning/economy.md US-6 (task 9797a0cfd9e6).
signal back_requested
signal item_purchased(listing_id: int)
@onready var back_button: Button = $VBox/HeaderHBox/BackButton
@onready var refresh_button: Button = $VBox/HeaderHBox/RefreshButton
@onready var balance_label: Label = $VBox/HeaderHBox/BalanceLabel
@onready var status_label: Label = $VBox/StatusLabel
# Filter inputs
@onready var search_edit: LineEdit = $VBox/FilterBar/SearchEdit
@onready var type_option: OptionButton = $VBox/FilterBar/TypeOption
@onready var min_price_edit: LineEdit = $VBox/FilterBar/MinPriceEdit
@onready var max_price_edit: LineEdit = $VBox/FilterBar/MaxPriceEdit
@onready var search_btn: Button = $VBox/FilterBar/SearchBtn
# Listings container
@onready var listings_container: GridContainer = $VBox/Scroll/ListingsGrid
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_balance()
search_listings()
func _get_api() -> Node:
if has_node("/root/ApiClient"):
return get_node("/root/ApiClient")
return null
func _setup_type_options() -> void:
if not type_option:
return
type_option.clear()
type_option.add_item("All Types", 0)
type_option.add_item("Avatar Item", 1)
type_option.add_item("Asset", 2)
type_option.add_item("Experience Pass", 3)
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 search_btn and not search_btn.pressed.is_connected(search_listings):
search_btn.pressed.connect(search_listings)
if search_edit and not search_edit.text_submitted.is_connected(func(_txt): search_listings()):
search_edit.text_submitted.connect(func(_txt): search_listings())
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_balance()
search_listings()
func refresh_balance() -> void:
var api = _get_api()
if not api or not api.has_token():
return
var res: Dictionary = await EconomyApiClient.get_wallet(api)
if res.ok and res.data is Dictionary:
var balance: int = int(res.data.get("balance", 0))
if balance_label:
balance_label.text = "Wallet: %d CR" % balance
func search_listings() -> void:
var api = _get_api()
if not api:
return
var q: String = search_edit.text.strip_edges() if search_edit else ""
var item_type: String = ""
if type_option and type_option.selected > 0:
match type_option.selected:
1: item_type = "avatar_item"
2: item_type = "asset"
3: item_type = "experience_pass"
var min_price: int = -1
if min_price_edit and min_price_edit.text.is_valid_int():
min_price = min_price_edit.text.to_int()
var max_price: int = -1
if max_price_edit and max_price_edit.text.is_valid_int():
max_price = max_price_edit.text.to_int()
show_status("Loading listings...")
var res: Dictionary = await EconomyApiClient.search_listings(api, q, item_type, min_price, max_price)
if res.ok and res.data is Dictionary:
var items: Array = res.data.get("listings", [])
render_listings(items)
show_status("Found %d active listings" % items.size())
else:
show_status("Search failed: " + res.detail, true)
func render_listings(listings: Array) -> void:
if not listings_container:
return
for c in listings_container.get_children():
listings_container.remove_child(c)
c.queue_free()
if listings.is_empty():
var empty_lbl := Label.new()
empty_lbl.text = "No listings matching your search."
empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
listings_container.add_child(empty_lbl)
return
for l in listings:
if not l is Dictionary:
continue
var card := _create_listing_card(l)
listings_container.add_child(card)
func _create_listing_card(listing: Dictionary) -> Control:
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(240, 200)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_top", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_bottom", 12)
card.add_child(margin)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 8)
margin.add_child(vbox)
var cat_item: Dictionary = listing.get("catalog_item", {})
var title_lbl := Label.new()
title_lbl.text = str(cat_item.get("name", "Unnamed Item"))
title_lbl.add_theme_font_size_override("font_size", 16)
vbox.add_child(title_lbl)
var type_lbl := Label.new()
var itype: String = str(cat_item.get("item_type", "")).replace("_", " ").capitalize()
type_lbl.text = itype
type_lbl.add_theme_color_override("font_color", Color(0.4, 0.7, 1.0))
type_lbl.add_theme_font_size_override("font_size", 12)
vbox.add_child(type_lbl)
var seller_lbl := Label.new()
var seller_name: String = str(listing.get("seller_username", "Unknown"))
seller_lbl.text = "By " + seller_name
seller_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
seller_lbl.add_theme_font_size_override("font_size", 12)
vbox.add_child(seller_lbl)
var desc_lbl := Label.new()
var desc: String = str(cat_item.get("description", ""))
if desc.length() > 60:
desc = desc.substr(0, 57) + "..."
desc_lbl.text = desc
desc_lbl.size_flags_vertical = Control.SIZE_EXPAND_FILL
desc_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
vbox.add_child(desc_lbl)
var bottom_hbox := HBoxContainer.new()
vbox.add_child(bottom_hbox)
var price: int = int(listing.get("price", 0))
var price_lbl := Label.new()
price_lbl.text = "%d CR" % price
price_lbl.add_theme_font_size_override("font_size", 16)
price_lbl.add_theme_color_override("font_color", Color(1.0, 0.8, 0.2))
price_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
bottom_hbox.add_child(price_lbl)
var buy_btn := Button.new()
buy_btn.text = "Buy"
buy_btn.custom_minimum_size = Vector2(80, 32)
var listing_id: int = int(listing.get("id", 0))
buy_btn.pressed.connect(_on_buy_clicked.bind(listing_id, str(cat_item.get("name", "Item")), price))
bottom_hbox.add_child(buy_btn)
return card
func _on_buy_clicked(listing_id: int, item_name: String, price: int) -> void:
var api = _get_api()
if not api or not api.has_token():
show_status("Please login to purchase items", true)
return
show_status("Purchasing '%s' for %d CR..." % [item_name, price])
var res: Dictionary = await EconomyApiClient.purchase_listing(api, listing_id)
if res.ok:
show_status("Successfully purchased '%s'! Item added to inventory." % item_name)
item_purchased.emit(listing_id)
refresh_all()
else:
show_status("Purchase failed: " + res.detail, true)
+72
View File
@@ -0,0 +1,72 @@
extends SceneTree
## Headless smoke test for MarketplaceUI scene and script.
func _initialize() -> void:
_run()
func _run() -> void:
await process_frame
print("[SMOKE] Starting MarketplaceUI smoke test...")
var scene = load("res://scenes/marketplace_ui.tscn")
if not scene:
printerr("[FAIL] Could not load scenes/marketplace_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.balance_label != null, "Missing balance_label")
assert(ui.status_label != null, "Missing status_label")
assert(ui.search_edit != null, "Missing search_edit")
assert(ui.type_option != null, "Missing type_option")
assert(ui.min_price_edit != null, "Missing min_price_edit")
assert(ui.max_price_edit != null, "Missing max_price_edit")
assert(ui.search_btn != null, "Missing search_btn")
assert(ui.listings_container != null, "Missing listings_container")
# Verify type options populated
assert(ui.type_option.item_count >= 4, "Type options not populated")
# Test rendering empty listings
ui.render_listings([])
assert(ui.listings_container.get_child_count() == 1, "Should show empty notice")
# Test rendering sample listings
var sample_listings = [
{
"id": 10,
"price": 200,
"seller_username": "merchant_bob",
"catalog_item": {
"name": "Wizard Robe",
"item_type": "avatar_item",
"description": "Flowing velvet robe"
}
},
{
"id": 11,
"price": 500,
"seller_username": "dungeon_master",
"catalog_item": {
"name": "Castle Map",
"item_type": "asset",
"description": "Detailed 3D fortress map"
}
}
]
ui.render_listings(sample_listings)
assert(ui.listings_container.get_child_count() == 2, "Should render 2 listing cards")
# Verify signals
assert(ui.has_signal("back_requested"), "Missing back_requested signal")
assert(ui.has_signal("item_purchased"), "Missing item_purchased signal")
ui.queue_free()
print("[SMOKE] MarketplaceUI smoke test PASSED")
quit(0)
+72
View File
@@ -0,0 +1,72 @@
extends SceneTree
## Headless smoke test for WalletUI scene and script.
func _initialize() -> void:
_run()
func _run() -> void:
await process_frame
print("[SMOKE] Starting WalletUI smoke test...")
var scene = load("res://scenes/wallet_ui.tscn")
if not scene:
printerr("[FAIL] Could not load scenes/wallet_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.balance_label != null, "Missing balance_label")
assert(ui.buy_100_btn != null, "Missing buy_100_btn")
assert(ui.buy_500_btn != null, "Missing buy_500_btn")
assert(ui.buy_1000_btn != null, "Missing buy_1000_btn")
assert(ui.buy_5000_btn != null, "Missing buy_5000_btn")
assert(ui.tx_container != null, "Missing tx_container")
assert(ui.prev_page_btn != null, "Missing prev_page_btn")
assert(ui.next_page_btn != null, "Missing next_page_btn")
assert(ui.page_label != null, "Missing page_label")
# Test rendering empty transactions
ui.render_transactions([])
assert(ui.tx_container.get_child_count() == 1, "Should have 1 empty notice child")
# Test rendering sample transactions
var sample_txs = [
{
"id": 1,
"created_at": "2026-09-15T12:00:00Z",
"type": "credit_purchase",
"amount": 500,
"balance_after": 500,
"reference_type": "payment",
"reference_id": "sess_123"
},
{
"id": 2,
"created_at": "2026-09-15T12:15:00Z",
"type": "marketplace_purchase",
"amount": -200,
"balance_after": 300,
"reference_type": "marketplace_listing",
"reference_id": "45"
}
]
ui.render_transactions(sample_txs)
assert(ui.tx_container.get_child_count() == 2, "Should have 2 transaction rows")
# Test paging logic
ui.total_tx_count = 45
ui.current_page = 0
ui._update_paging_controls()
assert(ui.prev_page_btn.disabled == true, "Prev should be disabled on page 0")
assert(ui.next_page_btn.disabled == false, "Next should be enabled on page 0")
assert("Page 1 of 3" in ui.page_label.text, "Page label text mismatch")
ui.queue_free()
print("[SMOKE] WalletUI smoke test PASSED")
quit(0)
+222
View File
@@ -0,0 +1,222 @@
extends Control
class_name WalletUI
## Wallet & Transaction History UI panel.
##
## Displays:
## - Current Credit balance
## - Credit pack quick purchases (100, 500, 1000, 5000 CR)
## - Paged ledger transaction history with status/amounts/references
##
## docs/planning/economy.md US-6 (task 34c725431645).
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 balance_label: Label = $VBox/BalanceCard/Margin/HBox/BalanceLabel
@onready var buy_100_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy100Btn
@onready var buy_500_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy500Btn
@onready var buy_1000_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy1000Btn
@onready var buy_5000_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy5000Btn
@onready var tx_container: VBoxContainer = $VBox/LedgerSection/Scroll/TxList
@onready var prev_page_btn: Button = $VBox/LedgerSection/PagingHBox/PrevPageBtn
@onready var next_page_btn: Button = $VBox/LedgerSection/PagingHBox/NextPageBtn
@onready var page_label: Label = $VBox/LedgerSection/PagingHBox/PageLabel
var _api_client: Node = null
var current_page: int = 0
const PAGE_SIZE: int = 20
var total_tx_count: int = 0
func _ready() -> void:
_api_client = _get_api()
_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 _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 buy_100_btn and not buy_100_btn.pressed.is_connected(_buy_pack.bind(100)):
buy_100_btn.pressed.connect(_buy_pack.bind(100))
if buy_500_btn and not buy_500_btn.pressed.is_connected(_buy_pack.bind(500)):
buy_500_btn.pressed.connect(_buy_pack.bind(500))
if buy_1000_btn and not buy_1000_btn.pressed.is_connected(_buy_pack.bind(1000)):
buy_1000_btn.pressed.connect(_buy_pack.bind(1000))
if buy_5000_btn and not buy_5000_btn.pressed.is_connected(_buy_pack.bind(5000)):
buy_5000_btn.pressed.connect(_buy_pack.bind(5000))
if prev_page_btn and not prev_page_btn.pressed.is_connected(_on_prev_page):
prev_page_btn.pressed.connect(_on_prev_page)
if next_page_btn and not next_page_btn.pressed.is_connected(_on_next_page):
next_page_btn.pressed.connect(_on_next_page)
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_wallet()
refresh_transactions()
func refresh_wallet() -> void:
var api = _get_api()
if not api or not api.has_token():
return
var res: Dictionary = await EconomyApiClient.get_wallet(api)
if res.ok and res.data is Dictionary:
var balance: int = int(res.data.get("balance", 0))
if balance_label:
balance_label.text = "%d CR" % balance
else:
show_status("Failed to fetch wallet: " + res.detail, true)
func refresh_transactions() -> void:
var api = _get_api()
if not api or not api.has_token():
return
var offset: int = current_page * PAGE_SIZE
var res: Dictionary = await EconomyApiClient.get_wallet_transactions(api, PAGE_SIZE, offset)
if res.ok and res.data is Dictionary:
var items: Array = res.data.get("items", [])
total_tx_count = int(res.data.get("total", 0))
render_transactions(items)
_update_paging_controls()
else:
show_status("Failed to fetch transactions: " + res.detail, true)
func _buy_pack(credits: int) -> void:
var api = _get_api()
if not api or not api.has_token():
show_status("Please login to purchase Credits", true)
return
show_status("Processing purchase of %d Credits..." % credits)
var res: Dictionary = await EconomyApiClient.purchase_credits(api, credits)
if res.ok and res.data is Dictionary:
var new_bal: int = int(res.data.get("new_balance", 0))
if balance_label:
balance_label.text = "%d CR" % new_bal
show_status("Successfully purchased %d Credits! Balance: %d CR" % [credits, new_bal])
refresh_transactions()
else:
show_status("Purchase failed: " + res.detail, true)
func render_transactions(tx_list: Array) -> void:
if not tx_container:
return
for c in tx_container.get_children():
tx_container.remove_child(c)
c.queue_free()
if tx_list.is_empty():
var empty_lbl := Label.new()
empty_lbl.text = "No transactions found."
empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
tx_container.add_child(empty_lbl)
return
for tx in tx_list:
if not tx 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(tx.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 type_lbl := Label.new()
type_lbl.text = str(tx.get("type", "")).replace("_", " ").capitalize()
type_lbl.custom_minimum_size = Vector2(160, 0)
hbox.add_child(type_lbl)
var amount: int = int(tx.get("amount", 0))
var amt_lbl := Label.new()
if amount > 0:
amt_lbl.text = "+%d CR" % amount
amt_lbl.add_theme_color_override("font_color", Color(0.2, 0.9, 0.2))
else:
amt_lbl.text = "%d CR" % amount
amt_lbl.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3))
amt_lbl.custom_minimum_size = Vector2(100, 0)
hbox.add_child(amt_lbl)
var bal_lbl := Label.new()
bal_lbl.text = "Bal: %s CR" % str(tx.get("balance_after", 0))
bal_lbl.custom_minimum_size = Vector2(100, 0)
bal_lbl.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
hbox.add_child(bal_lbl)
var ref_lbl := Label.new()
var ref_type: String = str(tx.get("reference_type", ""))
var ref_id: String = str(tx.get("reference_id", ""))
if ref_type != "":
ref_lbl.text = "%s #%s" % [ref_type, ref_id]
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)
tx_container.add_child(row)
func _update_paging_controls() -> void:
var total_pages: int = int(ceil(float(total_tx_count) / float(PAGE_SIZE)))
if total_pages < 1:
total_pages = 1
if page_label:
page_label.text = "Page %d of %d" % [current_page + 1, total_pages]
if prev_page_btn:
prev_page_btn.disabled = current_page <= 0
if next_page_btn:
next_page_btn.disabled = current_page >= total_pages - 1
func _on_prev_page() -> void:
if current_page > 0:
current_page -= 1
refresh_transactions()
func _on_next_page() -> void:
var total_pages: int = int(ceil(float(total_tx_count) / float(PAGE_SIZE)))
if current_page < total_pages - 1:
current_page += 1
refresh_transactions()